Jetpack 高级程序开发组件

Jetpack 简介

Jetpack 是一个开发组件工具集,它的主要目的是帮助我们编写出更加简洁的代码、并简化开发过程。

Jetpack 中的组件有一个特点,它们大部分不依赖于任何 Android 系统版本,这意味着这些组件通常是定义在 AndroidX 库当中的,并且拥有非常好的向下兼容性。

Jetpack 家族主要由基础、架构、行为、界面这 4 个部分组成。其中也不全是些新东西,像通知、权限、Fragment 都属于 Jetpack。


ViewModel

ViewModel 的一个重要作用就是可以帮助 Activity 分担一部分工作,它是专门用于存放与界面相关的数据的。

另一个非常重要的特性是它可以保证手机屏幕发生旋转时不会被重新创建,因为它的生命周期和 Activity 不同,只有当 Activity 退出时才会跟着 Activity 一起销毁,这样就避免了当手机屏幕发生旋转时,存放在 Activity 中的数据丢失问题。

Jetpack 高级程序开发组件

基本用法

首先要添加依赖:

implementation "androidx.lifecycle:lifecycle-extensions:2.2.0"
/**
 * 比较好的编程规范,
 * 是给每一个 Activity 和 Fragment 都创建一个对应的 ViewModel。
 */
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 的实例,而是要通过 ViewModelProviders 来获取。

        // 因为 ViewModel 有其独立的生命周期,并且比 Activity 长,(所以千万不要传递 Activity 过去)
        // 如果在 onCreate() 中创建了 ViewModel 实例,那么每次 onCreate() 执行时都会创建一个新的 ViewModel,
        // 这样当手机屏幕旋转时,就无法保留其中的数据了。
        // 注:ViewMolder 查看官网:已弃用 ViewModelProviders.of() 。
        // 您可以将 Fragment 或 FragmentActivity 传递给新 ViewModelProvider(ViewModelStoreOwner) 构造函数,以便在使用 Fragment 1.2.0 时实现相同的功能。
        // 如下:通过 ViewModelProvider 来获取 ViewModel 的实例: 
        // viewModel = ViewModelProvider(this).get(MainViewModel::class.java)
        viewModel = ViewModelProviders.of(this).get(MainViewModel::class.java)
        btnPlus.setOnClickListener{
            viewModel.counter++
            refreshCounter()
        }
        refreshCounter()
    }

    /**
     * 用来显示当前的计数
     */
    private fun refreshCounter() {
        tvInfo.text = viewModel.counter.toString()
    }
}

向 ViewModel 传递参数

/**
 * 借助 ViewModelProvider.Factory 实现向 MainViewModel 的构造函数中传递数据。
 */
class MainViewModelFactory(private val countReserved:Int) : ViewModelProvider.Factory{

    /**
     * 这里的 onCreate() 执行时机和 Activity 的生命周期无关,
     * 所以这里可直接创建 MainViewModel 实例。
     */
    override fun <T : ViewModel?> create(modelClass: Class<T>): T {
        return MainViewModel(countReserved) as T
    }
}
/**
 * countReserved 参数用于记录之前保存的数值,并在初始化时赋值给 counter 变量。
 */
class MainViewModel(countReserved:Int):ViewModel() {

    var counter = countReserved;
}
class MainActivity : AppCompatActivity() {

    lateinit var viewModel: MainViewModel
    lateinit var sp: SharedPreferences

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        // 获取 sp 实例
        sp = getPreferences(Context.MODE_PRIVATE)
        val countReserved = sp.getInt("count_reserved",0)
        // 在 of() 中传入 MainViewModelFactory 参数,只能通过这种方式来向 ViewModel 传递参数。
        viewModel = ViewModelProviders.of(this,MainViewModelFactory(countReserved))
            .get(MainViewModel::class.java)
        btnPlus.setOnClickListener{
            viewModel.counter++
            refreshCounter()
        }
        refreshCounter()
    }

    /**
     * 用来显示当前的计数
     */
    private fun refreshCounter() {
        tvInfo.text = viewModel.counter.toString()
    }

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

Lifecycles

Lifecycles 组件可以让任何一个类都能轻松感知到 Activity 的生命周期,同时又不需要在 Activity 中编写大量的逻辑处理。

/**
 * 实现 LifecycleObserver 接口
 *
 * 通过构造函数的 Lifecycle 对象,可以主动获知当前的生命周期状态,
 * 通过调用 lifecycle.currentState,它返回的生命周期状态是一个枚举类型。
 */
class MyObserver(val lifecycle: Lifecycle):LifecycleObserver {

    /**
     * 通过注解来感知 Activity 的生命周期
     * 类型一共七种,其中 ON_ANY 表示可以匹配 Activity 任意生命周期回调。
     */
    @OnLifecycleEvent(Lifecycle.Event.ON_START)
    fun activityStart(){
        Log.e("MyObserver","activityStart")
        Log.e("MyObserver",lifecycle.currentState.toString())
    }

    @OnLifecycleEvent(Lifecycle.Event.ON_STOP)
    fun activityStop(){
        Log.e("MyObserver","activityStop")
    }
}
class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        // 首先调用 LifecycleOwner 的 getLifecycle(),得到一个 Lifecycle 对象,
        // 然后调用它的 addObserver() 来观察 LifecycleOwner 的生命周期,
        // 再把 MyObserver 的实例传进去。
        // 在 AndroidX 库中已经自动帮我们完成这些了,不需要自己实现一个 LifecycleOwner。
        lifecycle.addObserver(MyObserver(lifecycle))
    }
}
Jetpack 高级程序开发组件

LiveData

LiveData 是 Jetpack 提供的一种响应式编程组件,它可以包含任何类型的数据,并在数据发生变化的时候通知观察者。

LiveData 特别适合与 ViewModel 结合使用,大多数情况下它也是使用在 ViewModel 当中的。

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

另外,由于要减少性能消耗,当 Activity 处于不可见状态时(比如息屏、或者被其他 Activity 遮挡),如果 LiveData 中的数据发生变化,是不会通知给观察者的。只有当 Activity 重新恢复可见状态时,才会将数据通知给观察者(如果 LiveData 发生过多次数据变化,此时只会将最新的那份通知给观察者,前面的数据相当于已经过期了,会被直接丢弃。),而 LiveData 之所以能够实现这种细节的优化,依靠的还是 Lifecycles 组件。

基本用法

/**
 * countReserved 参数用于记录之前保存的数值,并在初始化时赋值给 counter 变量。
 */
class MainViewModel(countReserved:Int):ViewModel() {

    /**
     * MutableLiveData 是一种可变的 LiveData
     * 主要有三种读写数据方法:
     * getValue(): 获取 LiveData 中包含的数据
     * setValue(): 用于给 LiveData 设置数据(主线程中调用)
     * postValue(): 用于给 LiveData 设置数据(非主线程中调用)
     * 下面用的只是 getValue(),setValue() 对应的语法糖写法。
     */
    var counter = MutableLiveData<Int>()

    /**
     * 在结构体中给 counter 设置数据,
     * 这样之前保存的计数值就可以在初始化时得到恢复。
     */
    init {
        counter.value = countReserved
    }

    /**
     * 计数加 1
     * 先获取 counter 中包含的数据,加 1 后再重新设置到 counter 中。
     */
    fun plusOne(){
        val count = counter.value ?: 0
        counter.value = count + 1
    }
}
class MainActivity : AppCompatActivity() {

    lateinit var viewModel: MainViewModel
    lateinit var sp: SharedPreferences

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
      
        lifecycle.addObserver(MyObserver(lifecycle))

        sp = getPreferences(Context.MODE_PRIVATE)
        val countReserved = sp.getInt("count_reserved",0)

        viewModel = ViewModelProviders.of(this,MainViewModelFactory(countReserved))
            .get(MainViewModel::class.java)

        btnPlus.setOnClickListener{
            viewModel.plusOne()
        }

        // observe() 接收两个参数:
        // 第一个参数是一个 LifecycleOwner 对象,因为 Activity 本身就是一个 LifecycleOwner 对象,所以直接传 this。
        // 第二个参数是一个 Observer 接口,当 counter 中包含的数据发生变化时,就会回调这里。
        // 因为这两个参数都是单抽象方法接口参数,所以要么同时使用函数式 API 的写法,要么都不使用。
//        viewModel.counter.observe(this, Observer { count ->
//            tvInfo.text = count.toString()
//        })
        // 添加依赖:implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.2.0"
        // 这个库提供了对 observe() 的语法扩展:
        viewModel.counter.observe(this) { count ->
            tvInfo.text = count.toString()
        }
    }

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

还有比较规范的做法,是永远只暴露不可变的 LiveData 给外部。这样在非 ViewModel 中就只能观察 LiveData 的数据变化,而不能给其设置数据。

class MainViewModel(countReserved:Int):ViewModel() {

    /**
     * 当外部调用 counter 变量时,实际上获得的是 _counter 的实例,但是无法给其设置数据。
     */
    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
    }
}

map 和 switchMap

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

data class User(var firstName:String,var lastName:String,var age:Int)
class MainViewModel: ViewModel() {

    /**
     * 声明为 private 保证数据封装性
     *
     * 这里还要注意一下声明顺序,如果在 userName 之下,map() 中的 userLiveData 会报必须初始化的提示。
     */
    private val userLiveData = MutableLiveData<User>()

    lateinit var user: User
  
    /**
     * 使用 Transformations.map() 对 LiveData 的数据类型进行转换
     *
     * map() 接收两个参数:
     * 第一个参数是原始的 LiveData 对象,
     * 第二个参数是一个转换函数,在其中编写具体的转换逻辑即可。
     * 这里是将 User 对象转换成一个只包含用户姓名的字符串。
     *
     * 当 userLiveData 的数据发生变化时,map() 会监听到变化并执行转换函数中的逻辑,
     * 再将转换之后的数据通知给 userName 的观察者。
     */
    val userName : LiveData<String> = Transformations.map(userLiveData){
            user -> "${user.firstName} ${user.lastName}"
    }

    init {
        user = User("xiao","wangwang",20)
        userLiveData.value = user
    }

    fun onPlus(){
        user = User("wang","xiaoxiao",20)
        userLiveData.value = user
    }
}
class MainActivity : AppCompatActivity() {

    lateinit var viewModel: MainViewModel

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
      
        viewModel = ViewModelProviders.of(this).get(MainViewModel::class.java)
      
        btnPlus.setOnClickListener{
            viewModel.onPlus()
        }

        viewModel.userName.observe(this){
            Log.e("TAG",it)
        }
    }
}

如果 ViewModel 中的某个 LiveData 对象是调用另外的方法获取的,那么便可以借助 switchMap() ,将这个 LiveData 对象转换成另外一个可观察的 LiveData 对象。

/**
 * 单例类
 */
object Repository {

    /**
     * 根据传入的参数去服务器请求或者到数据库中查找相应的 User 对象
     * 这里只是模拟演示
     *
     * 返回的是一个包含 User 数据的 LiveData 对象,而且每次调用都会返回一个新的 LiveData 实例。
     */
    fun getUser(userId:String):LiveData<User>{
        val liveData = MutableLiveData<User>()
        liveData.value = User(userId,userId,0)
        return liveData
    }
}
/**
 * 当外部调用 MainViewModel 的 getUser() 来获取用户数据时,并不会发起任何请求或者函数调用,
 * 只会将传入的 userId 值设置到 userIdLiveData 当中,
 * 一旦 userIdLiveData 的数据发生变化,那么观察 userIdLiveData 的 switchMap() 就会执行,
 * 并且调用编写的转换函数。然后在转换函数中调用 Repository.getUser() 获取真正的用户数据。
 * 同时,switchMap() 会将 Repository.getUser() 方法返回的 LiveData 对象转换成一个可观察的 LiveData 对象,
 * 对于 Activity 而言,只要去观察这个 LiveData 对象就可以了。
 */
class MainViewModel: ViewModel() {

    private val userIdLiveData = MutableLiveData<String>()

    private val refreshLiveData = MutableLiveData<Any?>()

    /**
     * switchMap() 接收两个参数:
     * 第一个参数是 userIdLiveData,switchMap() 会对它进行观察。
     * 第二个参数是一个转换函数,并且必须在这个转换函数中返回一个 LiveData 对象。
     */
    val user:LiveData<User> = Transformations.switchMap(userIdLiveData){ userId ->
        // 这里每次得到的都是一个新的实例
        Repository.getUser(userId)
    }

    val refreshResult = Transformations.switchMap(refreshLiveData){
        Repository.refresh()
    }

    fun getUser(userId:String){
        userIdLiveData.value = userId
    }

    /**
     * 当某个获取数据的方法没有参数时,可以这么写,
     * 然后在 Activity 中观察 refreshResult 这个 Livedata 对象即可,
     * 每次调用这个方法,观察者的回调函数中都会得到最新的数据。
     */
    fun refresh(){
        // 这里只是将原有数据(默认是空)重新设置了一遍,这样就能触发一次数据变化。
        refreshLiveData.value = refreshLiveData.value
    }
}
class MainActivity : AppCompatActivity() {

    lateinit var viewModel: MainViewModel

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        viewModel = ViewModelProviders.of(this).get(MainViewModel::class.java)

        btn.setOnClickListener{
            val userId = (0..10000).random().toString()
            viewModel.getUser(userId)
        }
        viewModel.user.observe(this, Observer { user ->
            tvInfo.text = user.firstName
        })
    }
}

Room

Room 是 Android 官方推出的一个 ORM 框架。

ORM(Object Relational Mapping)也叫对象关系映射。简单地将,我们使用的编程语言是面向对象语言,而使用的数据库则是关系型数据库,将面向对象的语言和面向关系的数据库之间建立一种映射关系,这就是 ORM 了。

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

使用 Room 进行增删改查

Room 的整体结构。它主要由 Entity、Dao 和 Database 三部分组成:

  • Entity。用于定义封装实际数据的实体类,每个实体类都会在数据库中有一张对应的表,并且表中的列是根据实体类中的字段自动生成的。
  • Dao。Dao 是数据访问对象的意思,通常会在这里对数据库的各项操作进行封装,在实际编程的时候,逻辑层就不需要和底层数据库打交道了,直接和 Dao 层进行交互即可。
  • Database。用于定义数据库中的关键信息,包括数据库的版本号、包含哪些实体类以及提供 Dao 层的访问实例。

首先需要添加插件和依赖:

apply plugin: 'kotlin-kapt'

dependencies {
  // 由于 Room 会根据项目中声明的注解来动态生成代码,因此这里一定要使用 kapt 引入 Room 的编译时注解库,
  // 而启用编译时注解功能则一定要先添加 kotlin-kapt 插件。(Java 项目使用 annotationProcessor)
    implementation "androidx.room:room-runtime:2.2.5"
    kapt "androidx.room:room-compiler:2.2.5"
}
/**
 * 定义 Entity
 * @Entity 注解声明它是一个实体类
 */
@Entity
data class User(var firstName:String,var lastName:String,var age:Int){

    /**
     * 一个良好的数据库编程建议是:
     * 给每个实体类都添加一个 id 字段,并将这个字段设为主键。
     * @PrimaryKey 设为主键
     * autoGenerate = true 使得主键的值是自动生成的
     */
    @PrimaryKey(autoGenerate = true)
    var id: Long = 0
}
/**
 * Dao 必须使用接口,使用 @Dao 注解声明这是一个 Dao
 * 内部则根据业务需求对各种数据库操作进行封装
 *
 * 使用非实体类参数来增删改查数据,必须编写 SQL 语句。
 */
@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
}
/**
 * 写法是非常固定的,只需定义好 3 个部分的内容:
 * 数据库版本号,包含哪些实体类,以及提供 Dao 层的访问实例。
 *
 * 使用 @Database 注解,并在其中声明了数据库的版本号以及包含哪些实体类。(多个实体类用逗号隔开)
 *
 * 必须继承自 RoomDatabase() 类,并且要使用 abstract 关键字声明为抽象类,
 * 然后提供相应的抽象方法,用户获取 Dao 实例,只需提供方法就可以,具体实现由 Room 在底层自动完成。
 */
@Database(version = 1,entities = [User::class])
abstract class AppDatabase :RoomDatabase(){

    abstract fun userDao():UserDao

    /**
     * 在结构体中编写了一个单例模式
     * 使用 instance 变量缓存 AppDatabase 的实例
     */
    companion object{
        private var instance:AppDatabase? =null

        @Synchronized
        fun getDatabase(context: Context):AppDatabase{

            /**
             * 如果 instance 变量不为空就直接返回,
             * 否则调用 Room.databaseBuilder() 来构建一个 AppDatabase 实例。
             */
            instance?.let {
                return it
            }

            /**
             * 接收三个参数:
             * 第一个一定要使用 context.applicationContext,避免内存泄漏的问题。
             * 第二个参数是 AppDatabase 的 class 类型
             * 第三个参数是数据库名
             */
            return Room.databaseBuilder(context.applicationContext,
                AppDatabase::class.java,"app_database").build().apply{
                instance = this
            }
        }
    }
}
class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        val userDao = AppDatabase.getDatabase(this).userDao()
        var user1 = User("Tom","Brady",40)
        var user2 = User("Tom","Hanks",63)
      
        addDataBtn.setOnClickListener{
            // 由于数据库操作属于耗时操作,Room 默认是不允许在主线程中进行数据库操作的。
            // 但为了测试方便,Room 还提供了一个更加简单的方法:
            // Room.databaseBuilder(context.applicationContext,
            //                AppDatabase::class.java,"app_database")
            //                .allowMainThreadQueries()
            //                .build()
            // 这样 Room 就允许在主线程中操作数据库了,但建议只在测试环境下使用。
            thread {
                // 将 User 对象插入到数据库中,并将 insertUser() 返回的主键 id 值赋值给原来的 User 对象
                // 因为使用更新和删除注解去操作数据时都是基于这个 id 值来操作的。
                user1.id = userDao.insertUser(user1)
                user2.id = userDao.insertUser(user2)
            }
        }
        updateDataBtn.setOnClickListener{
            thread {
                user1.age = 42
                userDao.updateUser(user1)
            }
        }
        deleteDateBtn.setOnClickListener{
            thread {
               userDao.deleteUserByLastName("Hanks")
            }
        }
        queryDateBtn.setOnClickListener{
            thread {
               for (user in userDao.loadAllUsers()){
                   Log.e("MainActivity",user.toString())
               }
            }
        }
    }
}

Room 的数据库升级

测试阶段使用的方法:

// 构建 AppDatabase 时,加入 fallbackToDestructiveMigration() 方法,
// 这样只要数据库进行了升级,Room 就会将当前数据库销毁,然后再重新创建。副作用就是之前数据库中的所有数据全部丢失。
Room.databaseBuilder(context.applicationContext,
    AppDatabase::class.java,"app_database")
    .fallbackToDestructiveMigration()
    .build()

正规写法:

@Entity
data class Book(var name:String,var pages:Int) {
    
    @PrimaryKey(autoGenerate = true)
    var id : Long = 0
}
@Dao
interface BookDao {

    @Insert
    fun insertBook(book: Book):Long

    @Query("select * from Book")
    fun loadAllBooks():List<Book>
}
/**
 * 将版本升级为 2,并添加实体类。
 */
@Database(version = 2,entities = [User::class,Book::class])
abstract class AppDatabase :RoomDatabase(){

    abstract fun userDao():UserDao
    abstract fun bookDao():BookDao

    companion object{

        /**
         * 实现匿名类 Migration,传入的参数代表当数据库版本从 1 升级到 2 时就执行这个匿名类中的升级逻辑。
         */
        val MIGRATION_1_2 = object : Migration(1,2){

            override fun migrate(database: SupportSQLiteDatabase) {
                // 由于要新增一张 Book 表,所以需要编写相应的建表语句。
                // 并且建表语句必须和 Book 实体类中声明的结构完全一致。
                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

        @Synchronized
        fun getDatabase(context: Context):AppDatabase{

            instance?.let {
                return it
            }

            /**
             * 添加 addMigrations(MIGRATION_1_2) 方法
             */
            return Room.databaseBuilder(context.applicationContext,
                AppDatabase::class.java,"app_database")
                .fallbackToDestructiveMigration()
                .addMigrations(MIGRATION_1_2)
                .build()
                .apply{
                instance = this
            }
        }
    }
}

如果只是向现有的表中添加新的列,那只需要使用 alert 语句修改表结构就可以。

@Entity
data class Book(var name:String,var pages:Int,var author:String) {

    @PrimaryKey(autoGenerate = true)
    var id : Long = 0
}
@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 text not null default 'unknown'")
            }
        }

        private var instance:AppDatabase? =null

        @Synchronized
        fun getDatabase(context: Context):AppDatabase{

            ...

            /**
             * 添加 addMigrations(MIGRATION_2_3) 方法
             */
            return Room.databaseBuilder(context.applicationContext,
                AppDatabase::class.java,"app_database")
                .fallbackToDestructiveMigration()
                .addMigrations(MIGRATION_1_2, MIGRATION_2_3)
                .build().apply{
                instance = this
            }
        }
    }
}

WorkManager

WorkManager 很适合用于处理一些要求定时执行的任务,它可以根据操作系统的版本自动选择底层是使用 AlarmManager 实现还是 JobScheduler 实现,从而降低了我们的使用成本。另外,它还支持周期性任务、链式任务处理等功能,是一个非常强大的工具。

但 WorkManager 和 Service 并不相同,也没有直接联系。Service 是 Android 系统的四大组件之一,它在没有被销毁的情况下是一直保持在后台运行的。而 WorkManager 只是一个处理定时任务的工具,它可以保证即使在应用程序退出甚至手机重启的情况下,之前注册的任务依然将会得到执行,因此 WorkManager 很适合用于执行一些定期和服务器进行交互的任务,比如周期性地同步数据,等等。

另外,使用 WorkManager 注册的周期性任务不能保证一定会准时执行,这并不是 bug,而是系统为了减少电量消耗,可能会将触发时间临近的几个任务放在一起执行,这样可以大幅度低减少 CPU 被唤醒的次数,从而有效延长电池的使用时间。

WorkManager 的所有功能,在国产手机上都有可能得不到正确的运行。这是因为绝大多数的国产手机厂商在进行 Android 系统定制时会增加一个一键关闭的功能,允许用户一键杀死所有非白名单的应用程序。而被杀死的应用程序既无法接收广播,也无法运行 WorkManager 的后台任务。

所以,建议是 WorkManager 可以用,但是千万别依赖它去实现什么核心功能,因为它在国产手机上可能会非常不稳定。

基本用法

// 添加依赖
implementation "androidx.work:work-runtime:2.3.4"

基本用法主要分三步:

  • 定义一个后台任务,并实现具体的任务逻辑。
  • 配置该后台任务的运行条件和约束信息,并构建后台任务请求。
  • 将该后台任务请求传入 WorkManager 的 enqueue() 中,系统会在合适的时间运行。
/**
 * 后台任务的写法非常固定
 * 首先每一个后台任务都必须继承自 Worker 类,并调用它唯一的构造函数。然后重写父类中的 doWork()。
 */
class SimpleWorker(context: Context,params:WorkerParameters):Worker(context,params) {


    /**
     * 编写具体的后台任务逻辑
     * 不会运行在主线程中,可放心执行耗时逻辑。
     */
    override fun doWork(): Result {
        Log.e("TAG","do work in SimpleWorker")
        // 返回一个 Result 对象,用于表示任务的运行结果。
        // 成功就 Result.success(),失败就 Result.failure()
        return Result.success()
    }
}
class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        doWorkBtn.setOnClickListener{
            // 配置该后台任务的运行条件和约束信息,这里只做最基本的配置。
            // OneTimeWorkRequest.Builder 是 WorkRequest.Builder 的子类,用于构建单次运行的后台任务请求。
            // 由于这里没有指定任何约束,因此后台任务基本上会在点击按钮之后立刻运行。
            val request = OneTimeWorkRequest.Builder(SimpleWorker::class.java).build()

            // WorkRequest.Builder 还有另一个子类 PeriodicWorkRequest.Build,
            // 可用于构建周期性运行的后台任务请求,但是为了降低设备性能消耗,构造函数中传入的运行周期间隔不能短于 15 分钟。
//            var request = PeriodicWorkRequest.Builder(SimpleWorker::class.java,15,TimeUnit.MINUTES).build()

            // 将构建出的后台任务请求传入 WorkManager 的 enqueue() 中,系统就会在合适的时间去运行了。
            WorkManager.getInstance(this).enqueue(request)
        }
    }
}

处理复杂任务

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        doWorkBtn.setOnClickListener{

            val request = OneTimeWorkRequest.Builder(SimpleWorker::class.java)
                // 延迟在 5分钟之后运行
                .setInitialDelay(5, TimeUnit.MINUTES)
                // 添加标签,最主要的一个功能就是可以通过标签来取消后台任务请求
                // WorkManager.getInstance(this).cancelAllWorkByTag("simple)
                // 即使没有标签,也可以通过 id 来取消后台任务请求。
                // WorkManager.getInstance(this).cancelWorkById(request.id)
                // 使用 id 只能取消单个后台任务请求,而使用标签,则可以将同一标签名的所有后台任务请求全部取消。
                // 还可以一次性取消所有后台任务请求:WorkManager.getInstance(this).cancelAllWork()
                .addTag("simple")
                // 当后台任务的 doWork() 中返回了 Result.retry(),那么可结合这个方法来重新执行任务。
                // 后两个参数用于指定在多久之后重新执行任务,时间最短不能少于 10 秒钟。
                // 第一个参数用于指定如果任务再次执行失败,下次重试的时间应该以什么样的形式延迟:
                // 可选值有两种:LINEAR(以线性的方式延迟)和 EXPONENTIAL(以指数的方式延迟)。
                .setBackoffCriteria(BackoffPolicy.LINEAR,10,TimeUnit.SECONDS)
                .build()


            // 监听后台任务的 doWork() 中返回的运行结果信息。
            // 还可以调用 getWorkInfosByTagLiveData() 监听同一标签名下所有后台任务请求的运行结果
            WorkManager.getInstance(this)
                .getWorkInfoByIdLiveData(request.id).observe(this){ workInfo ->
                    if (workInfo.state == WorkInfo.State.SUCCEEDED){
                        Log.e("TAG","do work succeeded")
                    }else if (workInfo.state == WorkInfo.State.FAILED){
                        Log.e("TAG","do work failed")
                    }
                }

            WorkManager.getInstance(this).enqueue(request)
            
            // WorkManager 的链式任务
            val sync = ...
            val compress = ...
            val upload = ...
            WorkManager.getInstance(this)
                // 开启一个链式任务,后续使用 then() 来连接。
                // 另外,必须前一个任务运行成功之后,后续任务才会执行。如果失败或是被取消了,后续任务就得不到运行。
                .beginWith(sync)
                .then(compress)
                .then(upload)
                .enqueue()
        }
    }
}

备注

参考资料

第一行代码(第3版)

上一篇:Android Jetpack 使用入门


下一篇:Android Jetpack 架构组件之 DataBinding