第一行代码Android 阅读笔记 第十三章(仅自用)

Jetpack

至此,已经具备了独立开发一款app的能力,但能够开发app和能够开发好的app不是一回事,这里的好是指代码质量优越,项目架构合理。

在2017年前,android没有制定规范,为了追求更高的代码质量,逐渐有第三方的社区和开发者将一些更加高级的项目架构引入到了android平台上,如MVP、MVVM等。使用这些架构开发出来的应用程序,在代码质量、可读性,易维护性等方面都有着更出色的表现。

2017年Google推出了一个官方的架构组件库——Architecture Components,旨在帮助开发者编写出更加符合高质量代码规范,更具有架构设计的应用程序。18年google退出开发组件工具集jetpack,并将architecture components作为jetpack一部分纳入其中,此后不断有新组件加入jetpack中。

jetpack简介

jetpack是一个开发组件工具集,主要目的是帮助我们编写更加简洁的代码,简化开发过程。jetpack组件有个特点:它们大部分不依赖于任何android系统版本,意味着这些组件通常是定义在androidx库中,并拥有很好的向下兼容性。

jetpack全家福如下
在这里插入图片描述
jetpack家族十分庞大,主要由基础、架构、行为、界面这4个部分组成。

最需我们关注的还是架构组件,android官网最推荐的项目架构是MVVM,因而jetpack中的许多架构组件是专门为MVVM架构量身打造的。

ViewModel

ViewModel应该算是jetpack中最重要的组件之一了。其实android平台上之所以会出现诸如MVP,MVVM之类的项目架构,就是因为在传统的开发模式下,activity的任务实在是太重要,既要负责逻辑处理,又要控制UI展示,甚至还得处理网络回调。

如果在大型项目中仍然使用这种写法就会使得项目十分臃肿并且难以维护。

而ViewModel一个重要作用就是可以帮助activity分担一部分工作,专门用于存放与界面相关的数据的,也就是说,只要是界面上能看到的数据,它的相关变量应该存放在ViewModel中,而不是Activity中,这样一定程度可以减少Activity中的逻辑。

另外,ViewModel还要一个非常重要的特性,当手机发生横竖屏旋转时,Activity会被重新创建,同时存放在activity中的数据也会丢失。而ViewModel的生命周期和Activity不同,它可以保证在手机屏幕发生旋转的时候不会被重新创建。只有当activity退出的时候才会跟着activity一起销毁。因此将与界面相关的变量存放在ViewModel当中,当旋转手机屏幕,界面上显示的数据也不会丢失。

在这里插入图片描述

//添加依赖
implementation "androidx.lifecycle:lifecycle-extensions:2.2.0"

class MainViewModel : ViewModel() {
 var counter = 0
}

class MainActivity : AppCompatActivity() {
 lateinit var viewModel: MainViewModel
   override fun onCreate(savedInstanceState: Bundle?) {
   super.onCreate(savedInstanceState)
   setContentView(R.layout.activity_main)
   viewModel = ViewModelProvider(this).get(MainViewModel::class.java)
   plusOneBtn.setOnClickListener {
       viewModel.counter++
       refreshCounter()
     }
   refreshCounter()
 }
  private fun refreshCounter() {
     infoText.text = viewModel.counter.toString()
  }
}

不可在onCreate直接创建ViewModel的实例,因为每次旋转后都会创建一次实例。

因此需要通过ModelProvider获取。

向ViewModel传递参数

借助ViewModel.Factory实现向viewmodel的构造函数中传递参数。

现在的计数器在程序退出后就会丢失数据,因此我们需要解决这个问题。

要在退出程序前保存计数,并在重新打开程序的时候读取之前保存的计数,并传递给MainViewModel,重新打开时读取viewmodel其中的数据。

class MainViewModel(countReserved: Int) : ViewModel() {
   var counter = countReserved
}

借助ViewModelProvider.Factory向MainViewModel的构造函数传递数据

class MainViewModelFactory(private val countReserved: Int) : ViewModelProvider.Factory {
//这个create方法创建了MainViewModel的实例,其执行时机与activity生命周期无关。
 override fun <T : ViewModel> create(modelClass: Class<T>): T {
      return MainViewModel(countReserved) as T
  }
}

class MainActivity : AppCompatActivity() {
  lateinit var viewModel: MainViewModel
  lateinit var sp: SharedPreferences
  override fun onCreate(savedInstanceState: Bundle?) {
     super.onCreate(savedInstanceState)
     setContentView(R.layout.activity_main)
     //获取sharedPreferences的实例,并读取之前保存的数据,
     sp = getPreferences(Context.MODE_PRIVATE)
     val countReserved = sp.getInt("count_reserved", 0)
     viewModel = ViewModelProvider(this, MainViewModelFactory(countReserved))
 .get(MainViewModel::class.java)
 ...
  clearBtn.setOnClickListener {
      viewModel.counter = 0
      refreshCounter()
 }
   refreshCounter()

 }
 override fun onPause() {
   super.onPause()
   sp.edit {
      putInt("count_reserved", viewModel.counter)
  }
 }
 ...
}

Lifecycles

在编写android应用时,经常遇到需要感知activity生命周期。如在某个界面发起了一条网络请求,当请求得到响应时,界面或许已经关闭了,这时不应该再对响应的结果进行处理。

在activity里面感知其生命周期非常简单,在一个非activity类中感知activity的生命周期,应该怎么写?

这类需求广泛存在,解决方案十分多:通过在activity中嵌入一个隐藏的fragment进行感知或通过手写监听器来进行感知。

以下是简单的手写监听器,很繁琐,实际上不会这么写,但反映了原理。
在这里插入图片描述
LifecycleObserver是一个空方法接口,无需重写任何方法。

通过注解感知activity的生命周期。有ON_CREATE、ON_START、ON_RESUME、ON_PAUSE、ON_STOP、ON_DESTROY,分别匹配Activity中相应的生命周期回调。
还有个ON_ONLY表示匹配任何生命周期回调。

class MyObserver : LifecycleObserver {

   @OnLifecycleEvent(Lifecycle.Event.ON_START)
   fun activityStart() {
     Log.d("MyObserver", "activityStart")
  }
  @OnLifecycleEvent(Lifecycle.Event.ON_STOP)
  fun activityStop() {
    Log.d("MyObserver", "activityStop")
 }
}

但此时activity的生命周期发生变化时没有通知MyObserver,而我们不想像前面一个个手动通知。

lifecycleOwner.lifecycle.addObserver(MyObserver())

此时需要借助LifecycleOwner的getLifecycle方法,得到Lifecycle对象,然后调用它的addObserver方法观察LifecycleOwner的生命周期,再把MyObserver实例传入。

而LifecycleOwner,不需要我们自己实现,当activity继承了AppCompatActivity、或者Fragment继承自androidx.fragment.app.Fragment的,它们本身就是一个LifecycleOwner实例
因此仅需

lifecycle.addObserver(MyObserver())

此时MyObserver虽然能感知到Activity的生命周期发生了变化,却无法主动获知当前的生命周期状态。因此在MyObserver的构造函数中添加Lifecycle对象即可。

有Lifecycle对象后,我们可通过lifecycle.currentState主动获得当前的生命周期状态。lifecycle.currentState返回的生命周期状态是一个枚举类型,一共有INITIALIZED、DESTROYED、CREATED、STARTED、RESUMED五种状态。
在这里插入图片描述
也就是说,当获取的生命周期状态是CREATED的时候,说明onCreate()方法已经执行了,但
是onStart()方法还没有执行。当获取的生命周期状态是STARTED的时候,说明onStart()
方法已经执行了,但是onResume()方法还没有执行,以此类推。

LiveData

LiveData是jetpack提供的一种响应式编程组件,包含任何类型的数据,并在数据发生变化时通知观察者,特别适合ViewModel结合使用。

前面设计的计数器是存在问题的,逻辑是每点击一次按钮,先给viewModel中的计数+1,然后立刻获取最新的计数,这种模式只能在单线程模式下正常工作,但当ViewModel内部开启线程执行一些耗时逻辑时,获取的数据还是之前的数据。

一直以来都是在activity中手动获取ViewModel中的数据,ViewModel却无法将数据的变化主动通知给activity。

不能将activity实例传给ViewModel,因为activity和viewmodel的生命周期长度不同,传递后可能会因为activity无法释放而造成内存泄漏。

可通过LiveData包装计时器计数来解决此问题。

修改ViewModel,将counter变量修改成一个MutableLiveData对象,指定泛型为Int,
MutableLiveData是一种可变的LiveData,主要有三个读写数据方法:getValue()、setValue()、postValue()
get用于获取LiveData中的数据,set用于在主线程中给LiveData设置数据,post用于在非主线程给LiveData设置数据。

class MainViewModel(countReserved: Int) : ViewModel() {
  val counter = MutableLiveData<Int>()
  init {
     counter.value = countReserved
  }
  fun plusOne() {
    val count = counter.value ?: 0
    counter.value = count + 1
 }
   fun clear() {
   counter.value = 0
 }
}


class MainActivity : AppCompatActivity() {
 ...
 override fun onCreate(savedInstanceState: Bundle?) {
 ...
   plusOneBtn.setOnClickListener {
     viewModel.plusOne()
   }
   clearBtn.setOnClickListener {
     viewModel.clear()
  }
   viewModel.counter.observe(this, Observer { count ->infoText.text = count.toString()
     })
 }
   override fun onPause() {
   super.onPause()
   sp.edit {
      putInt("count_reserved", viewModel.counter.value ?: 0)
    }
  }
}

observe方法接受两个参数,第一个参数是Lifecycleowner,即activity本身,第二个参数为Observer接口,当counter的数据发生改变,就会回调此接口。

observer接口是一个单抽象方法接口,只有一个待实现的onChanged方法,但在调用observe方法时却没有使用java函数式API的写法。

observe方法接收的另一个参数是Lifecycleowner,也是一个单抽象方法接口,当一个java方法同时接收两个单抽象方法接口参数时,要么同时使用函数式API的写法,要么都不使用函数式API的写法。而我们第一个参数传入的是this,因此只能这么写。

此时的livedata暴露了数据给外界可修改,破坏了其封装性。

class MainViewModel(countReserved: Int) : ViewModel() {
 val counter: LiveData<Int>
    get() = _counter
 private val _counter = MutableLiveData<Int>()
 init {
     _counter.value = countReserved
  }
 fun plusOne() {
   val count = _counter.value ?: 0
   _counter.value = count + 1
 }
 fun clear() {
   _counter.value = 0
 }
}

每次调用counter,得到的都是_counter,而_counter对于外部是不可见的,但又无法给counter设置数据,从而保证了ViewModel的数据封装性。

map

LiveData的基本用法为了能够应对各种不同的需求场景,提供了两种转换方法:map和switchMap。

map是将实际包含数据的LiveData和仅用于观察数据的LiveData进行转换的。

//
class MainViewModel(countReserved: Int) : ViewModel() {
  private val userLiveData = MutableLiveData<User>()
  val userName: LiveData<String> = Transformations.map(userLiveData) { user ->
  "${user.firstName} ${user.lastName}"
  }
 ...
}

map方法接收两个参数,参数一为原始的LiveData对象,参数二为转换函数,内含转换逻辑,此处将user对象转换为一个只包含用户姓名的字符串。

外部观测时只需观测userName,当userLiveData的数据发生变化时,map方法会监听到变化并执行转换函数中的逻辑,再将转换后的数据通知给userName的观察者。

switchMap

前面所用到的LiveData对象实例都是在ViewModel中创建的。然而在实际的项目中,不可能一直是这种理性情况,很有可能ViewModel中的某个LiveData对象是调用另外的方法获取的。

class MainViewModel(countReserved: Int) : ViewModel() {
 ...
  fun getUser(userId: String): LiveData<User> {
     return Repository.getUser(userId)//此处返回Repository对象,并非直接定义在ViewModel中。
  }
}

//但不能直接这样观测,因此每次调用返回的都是个新的实例,这样写会导致一直观察老的实例
viewModel.getUser(userId).observe(this){
   ....
}

此时可以使用switchMap(),将获取的LiveData对象转换为另一个可观察的LiveData对象。

class MainViewModel(countReserved: Int) : ViewModel() {
 ...
   private val userIdLiveData = MutableLiveData<String>()
   val user: LiveData<User> = Transformations.switchMap(userIdLiveData) { userId ->
      Repository.getUser(userId)
   }
   fun getUser(userId: String) {
     userIdLiveData.value = userId
  }
}

switch接受的两个参数map的差不多,将返回的livedata对象转换成另一个可观察的livedata 对象。

当调用getUser时,uerIdLiveData数据发生改变,观察userIdLiveData的switchMap方法就会执行,调用转换函数,获取真正的用户数据user对象。

总结:LiveData内部不会判断即将设置的数据是否与原数据相同,只要设置了setValue和postValue方法,就一定触发数据变化事件。

之所以LiveData能够成为Activity和ViewModel间的通信桥梁,并且没有内存泄漏风险,依赖的是Lifecycles组件。LiveData内部使用了Lifecycles组件来自我感知生命周期的变化,从而可以在activity销毁时能够及时释放引用,避免内存泄漏。

由于要减少性能消耗,当activity处于不可见状态时,当livedata发生变化,是不会通知观察者的。进当activity恢复到可见状态时,才会将数据通知观察者,且当这样的变化发生多次,仅会将最新那份数据通知给观察者,这样的细节实现依旧依赖Liftcycles组件。

Room

前面我们学习了SQLite数据库的使用方法,但当时仅使用了一些的原生API来进行数据的增删改查,这些原生API虽然简答好用,但用于大型项目会使得项目的代码变得混乱,除非进行了很好的封装,为此出现了很多专门为android数据库设计的ORM框架。

ORM也叫对象关系映射。我们使用的编程语言是面向对象语言,使用的数据库则是关系数据库,将面向对象和面向关系的数据库之间建立一种映射关系,这就是ORM了。

ORM框架使我们可以用面向对象的思维来和数据库进行交互,绝大多数情况下不用再和SQL语句打交道,也不用操作数据库的逻辑让项目的整体代码变得混乱。

android推出了一个ORM框架,并将其加入jetpack中,即Room。

Room的增删改查

room的整体结构主要由Enity、Dao和Dataabase这三部分。

  • entity:用于定义封装实际数据的实体类,每个实体类在数据库中有一张对应的表,且表中的列是根据实体类中的字段自动生成的。
  • Dao:数据访问对象。通常在这里对数据库的各项操作进行封装,实际编程时无需跟底层数据库打交道,与Dao层进行交互即可。
  • Database:用于定义数据库中的关键信息,包括数据库的版本号,包含哪些实体类以及提供Dao层的访问实例。

添加依赖:

apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
apply plugin: 'kotlin-kapt'
dependencies {
 ...
  implementation "androidx.room:room-runtime:2.1.0"
  kapt "androidx.room:room-compiler:2.1.0"
}

这里新增了一个kotlin-kapt插件,并在dependencies闭包中添加了两个Room的依赖库,由于Room会根据我们在项目中声明的注解来动态生成代码,因此这里一定要使用kapt引入Room的编译时注解库,而启用编译时注解功能则一定要先添加kotlin-kapt插件。kapt只能在kotlin项目中使用,java则使用annotationProcessor即可。

定义Entity
一个良好的数据库编程建议是,给每个实体类都添加一个id字段,并将此字段设为主键。

@Entity
data class User(var firstName: String, var lastName: String, var age: Int) {
 @PrimaryKey(autoGenerate = true)
  var id: Long = 0
}

此处在类名使用@Entity注解,将它声明成一个实体类,然后在user类中添加了一个id字段,并使用@PrimaryKey注解将它设为了主键,再把autoGenerate参数指定为true,使得主键的值是自动生成的。

定义Dao,所有访问数据库的操作都是在此封装的,覆盖所有的业务需求,使得业务方永远只需要与Dao层进行交互,而不必和底层的数据库打交道。

@Dao
interface UserDao {
 @Insert
 fun insertUser(user: User): Long
 @Update
 fun updateUser(newUser: User)
 @Query("select * from User")
 fun loadAllUsers(): List<User>
 @Query("select * from User where age > :age")
 fun loadUsersOlderThan(age: Int): List<User>
 @Delete
 fun deleteUser(user: User)
 @Query("delete from User where lastName = :lastName")
 fun deleteUserByLastName(lastName: String): Int
}

接口上使用了一个@Dao注解,这样Room才能将它识别成一个Dao。UserDao的内部就是根据业务需求对各种数据库操作进行的封装。数据库操作常有增删改查这四种,因此Room也提供了@Insert、@Delete、@Update、@Query这四种注解。

insertUser方法上面使用了@Insert注解,表示会将参数中传入的User对象插入数据库中,插入完成还会返回自动生成的主键id值返回,update方法上面使用了@Update注解,表示会将参数传入的User对象更新到数据库当中。deleteUser方法上面使用了@Delete注解,表示会将参数传入的User对象从数据库中删除,以上的几种数据库操作都是直接使用注解标识,无需编写SQL语句。

如果想要从数据库中查询数据,或者使用非实体类参数来增删改数据,就必须编写SQL语句。

比如我们在接口中定义了个loadAllUsers方法,用于从数据库中查询所有用户,如果只使用一个@Query注解,Room无法知道我们想要查询哪些数。

因此需要在@Querry中编写具体的SQL语句才行,我们还可以将方法中传入的参数指定到SQL语句汇总,比如loadUserOldeThan可查询所有年龄大于指定参数的用户。

另外,当使用的是非实体类参数来增删改查数据,那么也要编写SQL语句才行,且此时仅能使用@Query注解。

虽然使用Room需要经常编写SQL语句这一点不太友好,但SQL语句确实可以实现更加多样化的逻辑,但SQL语句确实可以实现更加多样化的逻辑。

且Room是支持编译时动态检查SQL语句语法的。也就是说,如果我们编写的SQL语句有错,编译时就会直接报错。

定义DataBase
三部分内容:数据库版本号、包含哪些实体类、以及提供Dao层的访问实例。

@Database(version = 1, entities = [User::class])
abstract class AppDatabase : RoomDatabase() {
  abstract fun userDao(): UserDao
  companion object {
    private var instance: AppDatabase? = null
 @Synchronized
 fun getDatabase(context: Context): AppDatabase {
    instance?.let {
      return it
 }
  return Room.databaseBuilder(context.applicationContext,
  AppDatabase::class.java, "app_database")
    . build().apply {
         instance = this
     }
   }
  }
}

在类的头部使用了@Databasez注解,并在注解中声明了数据库的版本号以及包含哪些实体类,多个实体类间用逗号隔开即可。

AppDatabase类必须继承自RoomDatabase类,并且一定要使用abstract关键字将其声明为抽象类,然后提供相应的抽象方法,用于获取之前的DAO实例,比如这里提供的userDao方法,不过只需要进行方法声明即可,具体方法实现由Room在底层自动完成。

紧接着在companion object结构体中编写了一个单例模式,因为原则上全局应该只存在一份AppDatabase实例,这里使用了instance变量来缓存AppDatabase实例,然后在getDataBase中判断instance变量是否为空,不为空则直接返回,为空则调用Room.databaseBuilder方法来构建一个AppDatabase实例。

dtabaseBuilder方法接受3个参数,

注意第一个参数一定要使用applicationContext,不能使用普通的context,不然容易导致内存泄漏情况,关于applicationContext的详细内容我们都会在稍后学习。

第二个参数是AppDatabase的Class类型,

第三个参数是数据库名,这些都比较简单,最后调用build方法完成构建,并将创建出来的实例赋值给instance变量,然后返回当前实例即可。

class MainActivity : AppCompatActivity() {
 ...
   override fun onCreate(savedInstanceState: Bundle?) {
 ...
     val userDao = AppDatabase.getDatabase(this).userDao()
     val user1 = User("Tom", "Brady", 40)
     val user2 = User("Tom", "Hanks", 63)
     addDataBtn.setOnClickListener {
           thread {
              user1.id = userDao.insertUser(user1)
              user2.id = userDao.insertUser(user2)
           }
       }
     updateDataBtn.setOnClickListener {
           thread {
              user1.age = 42
              userDao.updateUser(user1)
            }
        }
      deleteDataBtn.setOnClickListener {
           thread {
             userDao.deleteUserByLastName("Hanks")
           }
        }
       queryDataBtn.setOnClickListener {
           thread {
             for (user in userDao.loadAllUsers()) {
                 Log.d("MainActivity", user.toString())
              }
           }
     }
   }
 ...
}

数据库操作都为耗时操作,因此Room默认是不允许在主线程中进行数据库操作的,因此上述代码我们都将操作放到了子线程中,不过为了测试,room提供了一个更加简单的方法。

Room.databaseBuilder(context.applicationContext,AppDatabase::class.java,"app_database")
   .allowMainThreadQueries()
   .build()

此方法建议只在测试环境下使用。

可能会觉得Room十分繁琐,因为要实现entity、dao、database三件套,十分繁琐,还不如直接使用原生的SQLiteDatabase来得方便,但这样定义好之后,只需使用面向镀锡的思维去编写程序,而完全不用考虑数据库相关的逻辑和实现了。

在大型项目中,使用Room能够让你的代码拥有更加合理的分层与设计,同时也能让代码更加易于维护。

Room数据库升级

版本号+1,此处的升级是添加了新表,因此需要获取新的Dao和建立新表。
在companion object中,实现了Migration的匿名类,传入了1,2,表明当数据库版本从1升级到2的时候,就执行这个匿名类的升级逻辑。

在migrate中编写建表语句,且需要与实体类中的声明结构完全一致,否则Room会抛出异常。

在获取数据库实例时执行 .addMigrations(MIGRATION_1_2)。

现在当我们进行任何数据库操作时,room会根据当前数据库的版本号执行这些升级逻辑,从而让数据库始终保证是最新的版本。

@Database(version = 2, entities = [User::class, Book::class])
abstract class AppDatabase : RoomDatabase() {
  abstract fun userDao(): UserDao
  abstract fun bookDao(): BookDao
  companion object {
    val MIGRATION_1_2 = object : Migration(1, 2) {
      override fun migrate(database: SupportSQLiteDatabase) {
        database.execSQL("create table Book (id integer primary
        key autoincrement not null, name text not null,
        pages integer not null)")
     }
   }
 private var instance: AppDatabase? = null
 fun getDatabase(context: Context): AppDatabase {
     instance?.let {
        return it
    }
    return Room.databaseBuilder(context.applicationContext,
    AppDatabase::class.java, "app_database")
      .addMigrations(MIGRATION_1_2)
      .build().apply {
           instance = this
        }
     }
  }
}

当只是想向表中添加新列

@Database(version = 3, entities = [User::class, Book::class])
 abstract class AppDatabase : RoomDatabase() {
 ...
   companion object {
 ...
     val MIGRATION_2_3 = object : Migration(2, 3) {
        override fun migrate(database: SupportSQLiteDatabase) {
             database.execSQL("alter table Book add column author text not null
 default 'unknown'")
            }
        }
   private var instance: AppDatabase? = null
   fun getDatabase(context: Context): AppDatabase {
 ...
       return Room.databaseBuilder(context.applicationContext. AppDatabase::class.java, "app_database")
       .addMigrations(MIGRATION_1_2, MIGRATION_2_3)
       .build().apply {
       instance = this
      }
    }
  }
}

WorkManager

android基本每发布一个新版本,后台权限都会被进一步收紧。

为保证应用在不同系统版本上的兼容性,google推出了WorkManager组件。

WorkManager很适合用于处理一些要求定时执行的任务,它可以根据操作系统的版本自动选择底层是使用AlarmManager实现还是JobScheduler实现,从而降低我们的使用成本。

另外它还支持周期性任务、链式任务处理等功能,是一个非常强大的工具。

WorkManager与Service不同,也没有直接的联系。

Service是android系统的四大组件之一,它在没有被销毁情况下是一直保持在后台运行的。

而WorkManager只是一个处理定时任务的工具,它可以保证即时在应用退出甚至手机重启的情况下,之前注册的任务仍然会得到执行,因WorkManager很适合用于执行一些定期和服务器进行交互的任务,比如周期性同步数据等等。

另外使用workmanager注册的周期性任务不能保证一定会准时执行,系统为减少电量消耗,可能会将触发时间邻近的几个任务放在一起执行,这样可减少cpu被唤醒的次数,从而有效延长电池的使用时间。

添加依赖:

implementation "androidx.work:work-runtime:2.2.0"

workmanager的基本用法分三步:

  1. 定义一个后台任务,并实现具体的任务逻辑;
  2. 配置该后台任务的运行条件和约束信息,并构建后台任务请求。
  3. 将后台任务请求传入workmanager的enqueue方法中,系统会在合适的时间运行。

定义一个后台任务,每个后台任务都必须继承自worker类,并调用它唯一的构造函数,然后重写父类中的dowork方法,在这个方法中编写具体的后台任务逻辑。

doWork方法不会运行在主线程中,因此你可放心地在这里执行耗时逻辑,但该方法要求返回一个Result对象,用于表示任务的运行结果,成功就返回Result.success,失败则返回Result.failure。除此之外还有个Result.retry,也代表着失败,只是可以结合WorkRequest.Builder的setBackoffCriteria方法来重新执行任务。

class SimpleWorker(context: Context, params: WorkerParameters) : Worker(context, params) {
   override fun doWork(): Result {
        Log.d("SimpleWorker", "do work in SimpleWorker")
        return Result.success()
    }
}

配置后台任务
第一个仅进行了最基本的配置,只需将创建的后台任务所对应的clas对象传入OneTimeWorkRequest.Builder的构造函数中,调用build方法完成构建。

OneTimeWorkRequest.Builder是WorkRequest.Builder的子类,用于构建单次运行的后台任务请求。WorkRequest.Builder还有另一个子类PeriodicWorkRequest.Builder,用于构建周期性运行的后台请求,为了降低设备性能消耗。其构造函数中传入的运行周期间隔不能短于15分钟。

val request = OneTimeWorkRequest.Builder(SimpleWorker::class.java).build()
val request = PeriodcWorkRequest.Builder(SimpleWorkder::class.javav,15,TimeUnit.MINUTES).build()

将后台任务传入queue中

WorkManager.getInstance(context).enqueue(requset)

执行后台任务,此时任务在button被点击后就开始执行

doWorkBtn.setOnClickListener {
    val request = OneTimeWorkRequest.Builder(SimpleWorker::class.java).build()
    WorkManager.getInstance(this).enqueue(request)
 }

使用WorkManager处理复杂的任务

我们想要控制后台任务的具体运行时间,操作有下面这些:

让后台任务在指定的延迟的时间后运行,可自由选择时间的单位

val request = OneTimeWorkRequest.Builder(SimpleWorker::class.java)
  .setInitiaDelay(5,TimeUnit.MINUTES)
  .addTag("simple")
  .build()

给后台任务请求添加标签,最主要是可以通过标签来取消后台任务请求。

val request = OneTimeWorkRequest.Builder(SimpleWorker::class.java)
   ...
   .addTag("simple")
   .build()
//通过标签取消后台任务请求
WorkManager.getInstance(this).cancelAllWorkByTag("simple")
//可通过id取消
WorkManager.getInstance(this).cancelWorkById(request.id)
//取消所以后台任务请求
WorkManager.getInstance(this).cancelAllWord()

前面提到了,如果后台任务的dowork返回了Result.retry(),那么是可以结合setBackoffCriteria()来重新执行任务

val request = OneTimeWorkRequest.Builder(SimpleWorker::class.java)
 ...
 .setBackoffCriteria(BackoffPolicy.LINEAR, 10, TimeUnit.SECONDS)
 .build()

setBackoffCriteria接受3个参数,

参数2、3用于指定在多久后重新执行任务,时间最短不能少于10秒钟;

参数1用于指定 当任务再次执行失败,下次重试的时间应该以什么样的形式延迟。即当任务不断执行失败,应该考虑延迟重试时间,不然只是徒增性能消耗。可选值为LINEAR和EXPONENTIAL,前者表示重试时间以线性方式延迟,后者表示重试时间以指数的方式延迟。

Result.success和Result.failure用于通知任务运行结果的,对其进行监听。
此处调用了getWorkInfoByIdLiveData,返回了一个LiveData对象,然后就可以调用LiveData对象的observe方法来观察数据变化,以此监听后台任务的运行结果。
也可调用getWorkInfosByTagLiveData,监听同一标签名下所有后台任务请求的运行结果

WorkManager.getInstance(this)
 .getWorkInfoByIdLiveData(request.id)
 .observe(this) { workInfo ->
 if (workInfo.state == WorkInfo.State.SUCCEEDED) {
    Log.d("MainActivity", "do work succeeded")
 } 
 else if (workInfo.state == WorkInfo.State.FAILED) {
    Log.d("MainActivity", "do work failed")
   }
 }

workmanager有一个特色功能–链式任务。

假设定义了3个独立的后台任务:同步、压缩、上传,我们要想实现先同步、再压缩、再上传的功能,就可借助链式任务来实现

val sync = ...
val compress = ...
val upload = ...
WorkManager.getInstance(this)
 .beginWith(sync)
 .then(compress)
 .then(upload)
 .enqueue()

通过beginWith开启一个链式任务,通过then连接。链式任务要求在前一个后台任务运行成功后,下一个后台任务才会运行,也就是说前面的任务运行失败了,或被取消了,后面的后台任务都得不到运行了。

不要依赖workmanager实现核心功能,因为在国产系统中有一键关闭功能,一键杀死所有非白名单的应用程序,被杀死的应用程序无法接受广播,也无法运行WorkManager的后台任务。

使用DSL构建专有的语法结构

DSL全称是领域特定语言,是编程语言赋予开发者的一种特别能力,通过它可编写出一些看似脱离其原始语法结构的语法,从而构建出一种专有的语法结构。

前面使用的infix即属于DSL,此处使用高阶函数实现DSL。

前面添加依赖时,在gradle写入以下内容

dependencies {
 implementation 'com.squareup.retrofit2:retrofit:2.6.1'
 implementation 'com.squareup.retrofit2:converter-gson:2.6.1'
}

Gradle是一种基于Groovy语言的构建工具,因此上述的语法结构就是Groovy提供的DSL功能。

函数接受一个Dependency类中的函数类型参数,调用前获取Dependency实例即可,通过此实例调用函数型参数,执行lambda表达式,返回Dependency类中保存的依赖库集合返回。

fun dependencies(block: Dependency.() -> Unit): List<String> {
 val dependency = Dependency()
 dependency.block()
 return dependency.libraries
}

经过这样设计后,可使用这样的语法结构,因为dependencies函数拥有一个函数类型参数,因此传入一个lambda表达式,此时lambda表达式dependency类的上下文,可直接调用implementation添加依赖库。

dependencies {
 implementation 'com.squareup.retrofit2:retrofit:2.6.1'
 implementation 'com.squareup.retrofit2:converter-gson:2.6.1'
}

这种语法结构跟build.gradle文件中的语法结构不完全相同,主要是因为kotlin和groovy在语法层面有一定差别的。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值