美文网首页第一行代码(第2版)读书笔记
第一行代码读书笔记 15 -- 探究 Jetpack

第一行代码读书笔记 15 -- 探究 Jetpack

作者: 开心wonderful | 来源:发表于2020-07-13 13:42 被阅读0次

本篇文章主要介绍以下几个知识点:

夏日 (图片来源于网络)

Jetpack 是一个开发组件的工具集,其作用主要是编写出更简洁的代码,简化开发过程。这些组件通常是定义在 AndroidX 库当中的。

官方 Jetpack 目前的全家福如下:

Jetpack 全家福

目前 Android 官方最为推荐的项目架构是 MVVM,而 Jetpack 中的许多架构组件是专门为 MVVM 架构量身打造的。

1. ViewModel

在传统的开发模式下,Activity 扮演着多重角色,既要负责逻辑处理,又要控制 UI 展示,甚至还要处理网络回调等,在大型项目中显得过于臃肿且难以维护。

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

另外,ViewModel 的生命周期和 Activity 的不同,它可以保证在手机屏幕旋转时不会被重建,只有当 Activity 退出时才会跟着 Activity 一起销毁:

ViewModel 生命周期

1.1 ViewModel 的基本用法

首先在 build.gradle 中添加依赖:

implementation "androidx.lifecycle:lifecycle-extensions:2.2.0"

一般来说,会给每一个 Activity 和 Fragment 都创建一个对应的 ViewModel,如给 MainActivity 创建一个对应的 MainViewModel 如下:

class MainViewModel : ViewModel() { }

举个栗子,要实现一个计数器的功能,就可以在 ViewModel 中加入一个变量:

class MainViewModel : ViewModel() {
    var counter = 0   // 用于计数
}

界面上一个按钮和一个显示计数的 TextView

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <TextView
        android:id="@+id/counterText"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center_horizontal"
        android:textSize="32sp" />

    <Button
        android:id="@+id/plusButton"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_gravity="center_horizontal"
        android:text="plus one" />

</LinearLayout>

创建 ViewModel 的实例需要通过 ViewModelProvider 来获取,其语法规则如下:

ViewModelProvider(<你的 Activity 或 Fragment 实例>).get(<你的 ViewModel>::class.java)

接下来在 MainActivity 中创建 ViewModel 的实例和计数器逻辑:

class MainActivity : AppCompatActivity() {
    lateinit var viewModel: MainViewModel

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

        // 获取 ViewModel 实例
        viewModel = ViewModelProvider(this).get(MainViewModel::class.java)

        plusButton.setOnClickListener {
            // 点击按钮计数器加 1 并刷新计数
            viewModel.counter++
            refreshCounter()
        }
        refreshCounter()
    }

    private fun refreshCounter() {
        // 显示当前计数
        counterText.text = viewModel.counter.toString()
    }
}

注: 通过 ViewModelProvider 来获取实例是因为 ViewModel 有其独有的生命周期,其生命周期长于 Activity,若在 onCreate() 直接创建 ViewModel 的实例,那么每次 onCreate() 时都会创建一个新的实例,那手机屏幕旋转时就无法保留其中的数据了。

以上便是 ViewModel 的基本用法了。

1.2 向 ViewModel 传递参数

现有个需求,在程序退出时保存当前的计数,重新打开程序时读取保存的计数,并传递给 MainViewModel,那么可以修改 MainViewModel 如下:

// 在构造函数中添加 countReserved 参数用于记录之前保存的计数
class MainViewModel(countReserved: Int) : ViewModel() {
    var counter = countReserved   // 用于计数
}

ViewModel 的构造函数中传递参数需要借助 ViewModelProvider.Factory,其实现如下:

class MainViewModelFactory(private val countReserved: Int) : ViewModelProvider.Factory {
    override fun <T : ViewModel> create(modelClass: Class<T>): T {
        return MainViewModel(countReserved) as T
    }
}

注:ViewModelProvider.Factory 接口要求必须实现 create() 方法,这边可以创建 MainViewModel 的实例是因为 create() 方法的执行时机和 Activity 的生命周期无关,不会产生之前提到的问题。

接着修改 MainActivity 的代码如下:

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(SP_COUNT_RESERVED, 0)

        // 传入保存的计数并获取 ViewModel 实例
        viewModel = ViewModelProvider(this, MainViewModelFactory(countReserved)).get(MainViewModel::class.java)

        plusButton.setOnClickListener {
            // 点击按钮计数器加 1 并刷新计数
            viewModel.counter++
            refreshCounter()
        }
        clearButton.setOnClickListener {
            // 点击按钮计数器清零并刷新计数
            viewModel.counter = 0
            refreshCounter()
        }
        refreshCounter()
    }

    private fun refreshCounter() {
        // 显示当前计数
        counterText.text = viewModel.counter.toString()
    }

    override fun onPause() {
        super.onPause()
        // 退出保存计数
        sp.edit {
            putInt(SP_COUNT_RESERVED, viewModel.counter)
        }
    }

    companion object {
        const val SP_COUNT_RESERVED = "count_reserved"
    }
}

这样,退出程序并重新打开计数器的值就不会丢失了,只能点击 Clear 按钮才会清零。

2. Lifecycles

若有个需求,在一个非 Activity 的类中去感知 Activity 的生命周期,那么可以通过在 Activity 中嵌入一个隐藏的 Fragment 来进行感知,或通过手写监听器的方式来进行感知等。

比如通过手写监听器的方式来对 Activity 的生命周期进行感知:

class MyObserver {
    fun activityStart() { }
    fun activityStop() { }
}

class MainActivity : AppCompatActivity() {
    lateinit var observer: MyObserver

    override fun onCreate(savedInstanceState: Bundle?) {
        observer = MyObserver()
    }

    override fun onStart() {
        super.onStart()
        observer.activityStart()
    }

    override fun onStop() {
        super.onStop()
        observer.activityStop()
    }
}

上面这种方式虽然可以工作,但不够优雅,需要在 Activity 中编写大量的逻辑。

Lifecycles 组件就是为了解决这个问题出现的,它可以让任何一个类轻松感知到 Activity 的生命周期,同时无需在 Activity 中编写大量的逻辑处理。

下面举个栗子来说明 Lifecycles 组件的用法,新建一个 MyObserver 类实现 LifecycleObserver 接口:

class MyObserver : LifecycleObserver {
    // LifecycleObserver 是个空接口,只需接口实现声明,无需重写任何方法
}

接下来就可以在 MyObserver 中定义任何方法,若需要感知 Activity 的生命周期,还需借助额外的注解,如定义 activityStart()activityStop() 方法如下:

class MyObserver : LifecycleObserver {

    // 生命周期的类型有 7 种:
    // ON_CREATE, ON_START, ON_RESUME, ON_PAUSE, ON_STOP, ON_DESTROY 对应 Activity 中相应的生命周期回调
    // ON_ANY 表示可匹配 Activity 的任何生命周期回调
    @OnLifecycleEvent(Lifecycle.Event.ON_START)
    fun activityStart() {
        Log.d("MyObserver", "activityStart")
    }

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

这样,上面的 activityStart()activityStop() 方法就会分别在 Activity 的 onStart()onStop() 触发时执行。

此时,当 Activity 的生命周期发生变化时并不会去通知 MyObserver,还需借助 LifecycleOwner 让它得到通知,其语法结构如下:

// 调用 LifecycleOwner 的 getLifecycle() 方法得到 Lifecycle 对象
// 然后调用它的 addObserver() 方法来观察 LifecycleOwner 的生命周期
// 再把 MyObserver 的实例传进去
lifecycleOwner.lifecycle.addObserver(MyObserver())

对于 LifecycleOwner,只要 Activity 是继承自 AppCompatActivity 或 Fragment 是继承自 androidx.fragment.app.Fragment 的,那么它本身就是一个 LifecycleOwner 实例,即在 Activity 中直接调用即可:

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        // 传入 MyObserver 的实例就可以自动感知 Activity 的生命周期了
        lifecycle.addObserver(MyObserver())
    }
}

上面 MyObserver 虽然可以感知 Activity 是生命周期,却无法主动获取当前的生命周期状态,若需要主动获取则可在 MyObserver 的构造方法中传人 Lifecycle 对象:

class MyObserver(val lifecycle: Lifecycle) : LifecycleObserver {
   ...
}

有了 Lifecycle 对象后就可在任何地方调用 lifecycle.currentState 来主动获取当前的生命周期状态,lifecycle.currentState 返回的是一个枚举类型:

// 共 5 种状态:
INITIALIZED、DESTROYED、CREATED、STRARED、RESUMED

它们与 Activity 的生命周期回调所对应关系如下:

Activity 生命周期状态与事件的对应关系

3. LiveData

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

3.1 LiveData 的基本用法

前面计数器的栗子中,是在 Activity 中手动获取 ViewModel 中的数据,但 ViewModel 却无法将数据的变化主动通知给 Activity。

注:由于 ViewModel 的生命周期长于 Activity,若把 Activity 的实例传给 ViewModel 就可能会因为 Activity 无法释放而造成内存泄漏。

而用 LiveData 来包装计数器的计数,然后在 Activity 中去观察它,就可以将数据变化通知给 Activity 了。

修改 MainViewModel 的代码如下:

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

    // MutableLiveData 是一种可变的 LiveData,它主要有 3 种读写数据的方法:
    // getValue() 用于获取 LiveData 中包含的数据
    // setValue() 用于给 LiveData 设置数据,但只能在主线程中调用
    // postValue() 用于在非主线程中给 LiveData 设置数据
    var counter = MutableLiveData<Int>()

    init {
        counter.value = countReserved
    }

    // 计数器加 1
    fun plusOne() {
        val count = counter.value ?: 0
        counter.value = count + 1
    }

    // 计数器清零
    fun clear() {
        counter.value = 0
    }
}

接着修改 MainActivity 的代码如下:

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))

        // 获取 SharedPreferences 实例,读取之前保存的计数
        sp = getPreferences(Context.MODE_PRIVATE)
        val countReserved = sp.getInt(SP_COUNT_RESERVED, 0)

        // 获取 ViewModel 实例
        viewModel = ViewModelProvider(this, MainViewModelFactory(countReserved)).get(MainViewModel::class.java)

        plusButton.setOnClickListener {
            // 点击按钮计数器加 1
            viewModel.plusOne()
        }
        clearButton.setOnClickListener {
            // 点击按钮计数器清零
            viewModel.clear()
        }

        // 调用 observe 观察数据变化
        // observe() 方法接收两个参数:LifecycleOwner 对象 和 Observer 接口
        viewModel.counter.observe(this, Observer { count ->
            counterText.text = count.toString()
        })
    }

    override fun onPause() {
        super.onPause()
        // 保存计数
        sp.edit {
            putInt(SP_COUNT_RESERVED, viewModel.counter.value ?: 0)
        }
    }

    companion object {
        const val SP_COUNT_RESERVED = "count_reserved"
    }
}

注:上面 observe() 方法中没有用到函数式 API 的写法,当一个 Java 方法同时接收两个单抽象方法接口参数时,要么同时使用函数式 API 写法,要么都不用。由于第一个参数传 this,因此第二个参数就无法使用函数式 API 了。

当然若添加如下依赖库:

implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.2.0"

上面的 observe() 方法就可改为:

viewModel.counter.observe(this) { count ->
    counterText.text = count.toString()
}

以上就是 LiveData 的基本用法,但不是很规范,因为把 counter 这个可变的 LiveData 暴露给了外部,这样在 ViewModel 外面也可给 counter 设置数据,从而破坏了 ViewModel 的封装性,有一定的风险。

比较推荐的做法是,永远只暴露不可变的 LiveData 给外部,修改 MainViewModel 如下:

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

    val counter: LiveData<Int> get() = _counter

    // _counter 对外部不可见
    private val _counter = MutableLiveData<Int>()

    init {
        _counter.value = countReserved
    }

    // 计数器加 1
    fun plusOne() {
        val count = _counter.value ?: 0
        _counter.value = count + 1
    }

    // 计数器清零
    fun clear() {
        _counter.value = 0
    }
}

这样,当外部调用 counter 变量时,实际获得的是 _counter 的实例,但无法给 counter 设置数据,从而保证了 ViewModel 的数据封装性。

3.2 map 和 switchMap

LiveData 为了应付不同的场景需求,提供了两种转换方法:map()switchMap() 方法。

  • map() 方法: 将实际包含数据的 LiveData 和仅用于观察数据的 LiveData 进行转换。

举个栗子,定义一个 User 类如下:

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

将 User 对象转换成一个只包含用户姓名的字符串暴露给外部,就可以使用 map() 方法:

class MainViewModel : ViewModel() {

    private val _userLiveData = MutableLiveData<User>()
    // 当 _userLiveData 数据变化时,map() 方法会监听到并执行转换函数中的逻辑,
    // 然后把转换后的数据通知给 userName 的观察者
    val userName: LiveData<String> = Transformations.map(_userLiveData) {
        "${it.firstName} ${it.lastName}"
    }
}
  • switchMap() 方法:将转换函数中返回的 LiveData 对象转化成另外一个可观察的 LiveData 对象。

举个栗子,模拟 ViewModel 中的某个 LiveData 对象是调用另外的方法获取的,首先新建个单例类如下:

object Repository {
    // 模拟根据 userId 获取 User 对象
    // 每次调用 getUser() 方法都会返回一个新的 LiveData 实例
    fun getUser(userId: String): LiveData<User>{
        val liveData = MutableLiveData<User>()
        liveData.value = User(userId, userId, 18)
        return liveData
    }
}

借助 switchMap()方法,修改 MainViewModel 如下:

class MainViewModel : ViewModel() {

    // 用于观察 userId 的数据变化
    private val userIdLiveData = MutableLiveData<String>()

    // 传入 userId,switchMap() 方法会对它进行观察
    val user: LiveData<User> = Transformations.switchMap(userIdLiveData){
        Repository.getUser(it)
    }

    // 把传入的 userId 设置给 userIdLiveData, userIdLiveData 变化后 switchMap() 方法就会执行
    // 将 Repository.getUser() 返回的 LiveData 对象转换成一个可观察的 LiveData 对象
    fun getUser(userId: String){
        userIdLiveData.value = userId
    }
}

对于 Activity 来说,只有观察 user 这个 LiveData 对象即可,调用如下:

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)

        getUserButton.setOnClickListener {
            // 点击按钮传入随机数作 userId 获取用户数据
            // 数据获取完成后,可观察的 LiveData 对象的 observe() 方法将会得到通知,显示名字
            val userId = (0..10000).random().toString()
            viewModel.getUser(userId)
        }

        viewModel.user.observe(this){
            userText.text = it.firstName
        }     
    }
}

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

另外,当 Activity 不可见状态时,若 LiveData 的数据发生了变化,是不会通知给观察者的,只有当 Activity 恢复可见状态时,才会通知(只有最新的那份数据才会通知给观察者),减少性能消耗。

4. Room

ORM(Object Relational Mapping) 叫对象关系映射。将面向对象的语言和面向关系的数据库之间建立一种映射关系,就是 ORM 了,Jetpack 中的 Room 就是一个 ORM 框架。

4.1 使用 Room 进行增删改查

Room 主要由3部分组成:

  • Entity :定义封装实际数据的实体类,每个实体类都会在数据库中有一张对于的表,并且表中的列是根据实体类中的字段自动生成的。

  • Dao:数据访问对象,封装对数据库的各项操作,在编程时逻辑层直接和 Dao 层交互即可。

  • Database:定义数据库中的关键信息,包括数据库版本号、包含的实体类以及提供 Dao 层的访问实例。

要使用 Room,需要在 build.gradle 中添加如下依赖:

apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
apply plugin: 'kotlin-kapt'

dependencies {
    // Room
    implementation "androidx.room:room-runtime:2.2.5"
    kapt "androidx.room:room-compiler:2.2.5"
}

下面举个栗子来实现 Room 的3个组成部分,首先是用 @Entity 注解来定义 Entity 实体类如下:

@Entity
data class User(var firstName: String, var lastName: String, var age: Int) {
    // 把 id 字段设为主键,并自动生成
    @PrimaryKey(autoGenerate = true)
    var id: Long = 0
}

接着使用 @Dao 注解来定义 Dao,新建 UserDao 接口如下:

@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)

    // 通过 lastName 来删除对象
    @Query("delete from User where lastName = :lastName")
    fun deleteUserByLastName(lastName: String): Int
}

最后,使用 @Database 注解来定义 Database,只需定义好数据库版本号、包含哪些实体类、提供 Dao 层的访问实例,如下:

// 注解中声明版本号及包含的实体类,多个实体类用逗号隔开即可
@Database(version = 1, entities = [User::class])
abstract class AppDatabase : RoomDatabase() {

    abstract fun userDao(): UserDao

    companion object {
        private var instance: AppDatabase? = null

        fun getDatabase(context: Context): AppDatabase {
            // 不为空直接返回
            instance?.let { return it }
            // 否则构建一个实例
            return Room.databaseBuilder(
                context.applicationContext,// 第一个参数一定要使用 applicationContext,否至容易出现内存泄漏
                AppDatabase::class.java,   // 第二个参数是 AppDatabase 的 Class 类型
                "app_database"             // 第三个参数是数据库名
            ).build().apply {
                instance = this
            }
        }
    }
}

在 Activity 中的使用如下:

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        // 获取 UserDao 的实例
        val userDao = AppDatabase.getDatabase(this).userDao()
        val user1 = User("李", "小兰", 18)
        val user2 = User("王", "小红", 10)

        // 新增
        addUserButton.setOnClickListener {
            thread {
                user1.id = userDao.insertUser(user1)
                user2.id = userDao.insertUser(user2)
            }
        }
        // 更新
        updateUserButton.setOnClickListener {
            thread {
                user1.age = 20
                userDao.updateUser(user1)
            }
        }
        // 删除
        deleteUserButton.setOnClickListener {
            thread {
                userDao.deleteUserByLastName("小红")
            }
        }
        // 查询
        queryUserButton.setOnClickListener {
            thread {
                for (user in userDao.loadAllUsers()) {
                    Log.d("MainActivity", user.toString())
                }
            }
        }
    }
}

4.2 Room 的数据库升级

在开发测试阶段,在构建 AppDatabase 实例时,添加 fallbackToDestructiveMigration() 方法如下:

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

这样只要数据库进行了升级, Room 就会将当前数据库销毁(数据全丢失了),再重建,实现自动升级数据库,但这只适合用在开发测试阶段,下面介绍在 Room 中升级数据库的正规写法。

如在数据库中添加一张 Book 表如下:

@Entity
data class Book(var name: String, var page: 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>
}

接下来要升级数据库,需要修改 AppDatabase 如下:

// 将版本号升级成了2,并将 Book 类添加到实体类声明中
@Database(version = 2, entities = [User::class, Book::class])
abstract class AppDatabase : RoomDatabase() {

    abstract fun userDao(): UserDao

    abstract fun bookDao(): BookDao

    companion object {

        // 当数据库版本从 1 升级到 2 时就执行这个匿名类 Migration 中的逻辑
        private 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)   // 传入 MIGRATION_1_2
                .build().apply {
                    instance = this
                }
        }
    }
}

这样 Room 就会自动根据当前数据库的版本号执行升级逻辑,让数据库始终是最新版本。

当向现有表中添加新的列而不是新增一张表时,使用 alert 语句修改表结构即可,如在 Book 中添加一个 author 字段:

@Entity
data class Book(var name: String, var page: Int, var author: String) {
    @PrimaryKey(autoGenerate = true)
    var id: Long = 0
}

那么对应的数据库表升级,修改 AppDatabase 如下:

// 将版本号升级成了3
@Database(version = 3, entities = [User::class, Book::class])
abstract class AppDatabase : RoomDatabase() {

    abstract fun userDao(): UserDao

    abstract fun bookDao(): BookDao

    companion object {

        // 当数据库版本从 1 升级到 2 时就执行这个匿名类 Migration 中的逻辑
        private 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)")
            }
        }

        // 当数据库版本从 2 升级到 3 时执行
        private 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 {
            instance?.let { return it }
            return Room.databaseBuilder(context.applicationContext, AppDatabase::class.java, "app_database")
                .addMigrations(MIGRATION_1_2, MIGRATION_2_3) // 传入 MIGRATION_1_2, MIGRATION_2_3
                .build().apply {
                    instance = this
                }
        }
    }
}

5. WorkManager

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

WorkManager 和 Service 并不相同,也没直接的联系,Service 是四大组件之一,在没被销毁时一直运行在后台。

另外,系统为了减少电量消耗,使用 WorkManager 注册的周期性任务不能保证一定会准时执行,可能会将触发时间临近的几个任务放在一起执行。

5.1 WorkManager 的基本用法

要使用 WorkManager,需要在 build.gradle 中添加如下依赖:

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

其基本用法主要有以下三步:

  • (1)定义一个后台任务,并实现具体的任务逻辑

  • (2)配置该后台任务的运行条件和约束信息,并构建后台任务请求

  • (3)将该后台任务请求传入 WorkManager 的enqueue()方法中,系统会在合适的时间运行

下面举个栗子,首先定义一个后台任务如下:

// 每一个后台任务都必须继承自 Worker 类,并调用它唯一的构造函数,然后重写 doWork() 方法
class SimpleWorker(context: Context, params: WorkerParameters) : Worker(context, params) {
    // doWork 方法不会运行在主线程中,可处理耗时操作
    override fun doWork(): Result {
        Log.d("SimpleWorker", "do work in SimpleWorker")
        // 返回一个 Result 对象,成功:Result.success() 失败:Result.failure() 或 Result.retry()
        return Result.success()
    }
}

接着配置运行条件和约束信息:

// WorkRequest.Builder 有两个子类:
// OneTimeWorkRequest.Builder:构建单次运行的后台任务请求
val request = OneTimeWorkRequest.Builder(SimpleWorker::class.java).build()

// PeriodicWorkRequest.Builder:构建周期性运行的后台任务请求
// 如构建运行周期间隔不能短于15分钟的后台任务
val request = PeriodicWorkRequest.Builder(SimpleWorker::class.java, 15, TimeUnit.MINUTES).build()

最后把构建出的任务传入 WorkManager 中:

WorkManager.getInstance(context).enqueue(request)

如在 Activity 中调用:

class MainActivity : AppCompatActivity() {

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

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

    }
}

5.2 使用 WorkManager 处理复杂的任务

WorkManager 除了运行时间外,还允许控制许多其他的东西,如下:

val request = OneTimeWorkRequest.Builder(SimpleWorker::class.java)
    // 指定延迟时间后运行
    .setInitialDelay(5, TimeUnit.MINUTES)
    // 添加标签
    .addTag("simple") 
    // 若任务返回了 Result.retry(),可以结合 setBackoffCriteria() 方法来重新执行任务 
    // 第一个参数有两种:LINER 重试时间以线性方式延迟,EXPONENTIAL 重试时间以指数方式延迟
    .setBackoffCriteria(BackoffPolicy.LINEAR, 10, TimeUnit.SECONDS)
    .build()
        
// 通过标签取消任务
WorkManager.getInstance(this).cancelAllWorkByTag("simple")
// 通过 id 取消任务
WorkManager.getInstance(this).cancelWorkById(request.id)
// 取消全部任务
WorkManager.getInstance(this).cancelAllWork()

另外,可以调用 getWorkInfoByIdLiveData()getWorkInfoByTagLiveData() 方法来监听任务运行结果,如:

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

再介绍个 WorkManager 中比较有特色的功能——链式任务

如定义了3个独立后台任务:同步数据、压缩数据、上传数据,要想先同步、再压缩、最后上传就可以借助链式任务来实现:

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

注:必须前一个任务成功后下一个任务才会运行,若某个任务失败或取消了,那么接下来的任务都不会运行。

注:WorkManager 在国产手机上可能会非常不稳定,不要依赖它去实现核心功能。

本篇文章就介绍到这。

相关文章

网友评论

    本文标题:第一行代码读书笔记 15 -- 探究 Jetpack

    本文链接:https://www.haomeiwen.com/subject/hupbcktx.html