《Android》Chap.13 Jetpack

Jetpack简介

Jetpack是一个开发组件工具集,它的主要目的是帮助开发者编写更加简洁的代码,并简化开发过程。另外,它不依赖于任何Android系统版本,拥有很好的向下兼容性。

ViewModel

ViewModel的作用就是可以帮助Activity分担一部分工作,它是专门用于存放与界面相关的数据的。也就是说,只要是界面上能看得到的数据,它的相关变量都应该存放在ViewModel中,而不是Activity中,这样可以在一定程度上减少Activity中的逻辑。
另外,ViewModel的生命周期和Activity不同,比如,当手机发生横竖屏旋转的时候,Activity会被重新创建,同时存放在Activity中的数据也 会丢失。而ViewMode可以保证在手机屏幕发生旋转的时候不会被重新创建,只有当Activity退出的时候才会跟着Activity一起销毁。因此,将与界面相关的变量存放在ViewModel当中,这样即使旋转手机屏幕,界面上显示的数据也不会丢失。

在这里插入图片描述

基本用法

在使用前要先添加依赖包

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

activity_main.xml中添加一个idinfoTextTextView用于记数,再添加一个idplusOneBtnButton用于让数字增加。
在新建一个MainViewModel

class MainViewModel :ViewModel() {
    var counter = 0 //记录infoText的数字
}

MainActivity中添加具体逻辑

class MainActivity : AppCompatActivity() {

    private lateinit var mainBinding: ActivityMainBinding
    lateinit var viewModel: MainViewModel
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        mainBinding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(mainBinding.root)

        viewModel = ViewModelProvider(this).get(MainViewModel::class.java)
        mainBinding.plusOneBtn.setOnClickListener {
            viewModel.counter ++
            refreshCounter()
        }
        refreshCounter()
    }

    private fun refreshCounter() {
        mainBinding.infoText.text = viewModel.counter.toString()
    }
}

然后运行时,点击按钮后及其旋转屏幕也不会使记数消失。

注意

在使用时一定不能直接创建ViewModel的实例,而是通过ViewModelProvider来获取它的实例

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

因为ViewModel有其独立的生命周期,并且其生命周期要长于Activity。如果在onCreate()方法中创建ViewModel的实例,那么每次onCreate()方法执行的时候,ViewModel都会创建一个新的实例,这样当手机屏幕发生旋转的时候,就无法保留其中的数据了。

向ViewModel传递参数

借助ViewModelProvider.Factory就可以实现了。
比如,实现退出程序后又重新打开的情况下,数据仍然不会丢失。实现这个功能需要在退出程序的时候对当前的计数进行保存,然后在重新打开程序的时候读取之前保存的计数,并传递给MainViewModel
首先需要修改MainViewModel中的代码

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

再新建一个MainViewModelFactory

class MainViewModelFactory(private val countReserved: Int) : ViewModelProvider.Factory{

    //create()方法的执行时机与Activity的生命周期完全无关
    override fun <T : ViewModel?> create(modelClass: Class<T>): T {
        return MainViewModel(countReserved) as T
    }

}

然后在activity_main.xml中添加一个idclearBtn的按钮用于清零数据
最后修改MainActivity中的代码

class MainActivity : AppCompatActivity() {

    private lateinit var mainBinding: ActivityMainBinding
    lateinit var viewModel: MainViewModel
    lateinit var sp : SharedPreferences

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        mainBinding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(mainBinding.root)

        //读取之前的记录 如果没有的话就为0
        sp = getPreferences(Context.MODE_PRIVATE)
        val countReserved = sp.getInt("count_reserved",0)

        //将读取到的记数值传给MainViewModelFactory的构造函数
        viewModel = ViewModelProvider(this,MainViewModelFactory(countReserved))
            .get(MainViewModel::class.java)

        mainBinding.plusOneBtn.setOnClickListener {
            viewModel.counter ++
            refreshCounter()
        }

        mainBinding.clearBtn.setOnClickListener {
            viewModel.counter = 0
            refreshCounter()
        }

        refreshCounter()
    }

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

    private fun refreshCounter() {
        mainBinding.infoText.text = viewModel.counter.toString()
    }
}

这样当运行时点击按钮退出后在进入记数依然保存。

Lifecycles

Lifecycles可以让任何一个类都能轻松感知到Activity的生命周期,同时也不需要在Activity中编写大量的逻辑处理。
在使用时,要新创建一个名字为MyObserver的类

class MyObserver : LifecycleObserver {

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

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

代码中的activityStart()activityStop()法就应该分别在ActivityonStart()onStop()触发的时候执行,这个时候就得借助LifecycleOwner

lifecycleOwner.lifecycle.addObserver(MyObserver())

虽然可以自己去实现一个LifecycleOwner,但通常情况下这是完全没有必要的。因为只要是继承自AppCompatActivityActivity,或是继承自androidx.fragment.app.FragmentFragment, 那么它们本身就是一个LifecycleOwner的实例,这部分工作已经由AndroidX库自动完成了。
所以只需要在想监听的activity中加入一行代码就可以了
在这里插入图片描述
这样尝试运行和退出,就会有对应的日志打印。
在这里插入图片描述
此外,Lifecycle还可以主动获知当前activity的生命周期。
只需要在MyObserver的构造函数中将Lifecycle对象传进来即可。
在这里插入图片描述
有了Lifecycle对象之后,就可以在任何地方调用lifecycle.currentState来主动获知当前的生命周期状态。lifecycle.currentState返回的生命周期状态是一个枚举类型,一共有INITIALIZEDDESTROYEDCREATEDSTARTEDRESUMED,5种状态。
在这里插入图片描述
比如,当返回值为CREATED时,就说明onCreate()方法已经执行了,但onStart()方法还没执行。

LiveData

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

基本用法

比如刚刚的计数,是在Activity中手动获取ViewModel中的数据这种交互方式,但是ViewModel却无法将数据的变化主动通知给Activity
如果将计数器的计数使LiveData来包装,然后在Activity中去观察它,就可以主动将数据变化通知给Activity了。

修改一下MainViewModel中的代码

class MainViewModel(countReserved: Int) : ViewModel() {
    var counter = MutableLiveData<Int>()

    init { //在初始化时恢复之前的计数
        counter.value = countReserved
    }

    fun plusOne(){
        //数值为空时默认为0
        val count = counter.value ?:0
        counter.value = count + 1
    }

    fun clear(){
        counter.value = 0
    }
}

注意:如果需要在子线程中给LiveData设置数据,一定要调用postValue()方法,而不能再使用setValue()方法,否则会发生崩溃。

对应的再修改一下MainActivity中的代码

class MainActivity : AppCompatActivity() {

    private lateinit var mainBinding: ActivityMainBinding
    lateinit var viewModel: MainViewModel
    lateinit var sp : SharedPreferences

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        mainBinding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(mainBinding.root)

        //读取之前的记录 如果没有的话就为0
        sp = getPreferences(Context.MODE_PRIVATE)
        val countReserved = sp.getInt("count_reserved",0)

        //将读取到的记数值传给MainViewModelFactory的构造函数
        viewModel = ViewModelProvider(this,MainViewModelFactory(countReserved))
            .get(MainViewModel::class.java)

        mainBinding.plusOneBtn.setOnClickListener {
            viewModel.plusOne()
        }

        mainBinding.clearBtn.setOnClickListener {
            viewModel.clear()
        }

		//当counter中的数据发生变化时,就会回调到这里
        viewModel.counter.observe(this, Observer { count ->
            mainBinding.infoText.text = count.toString()
        })
    }

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

这样就可以正常工作了。

但它还不是最规范的做法。
主要的问题就在于将counter这个可变的LiveData暴露给了外部。这样即使是在ViewModel的外面也是可以给counter设置数据的,从而破坏了ViewModel数据的封装性,同时也可能带来一定的风险。
比较推荐的做法是,永远只暴露不可变的LiveData给外部。这样在非ViewModel中就只能观察LiveData的数据变化,而不能给LiveData设置数据。

class MainViewModel(countReserved: Int) : ViewModel() {
    
    val counter: LiveData<Int>
        get() = _counter
    
    private val _counter = MutableLiveData<Int>()

    init { //在初始化时恢复之前的计数
        _counter.value = countReserved
    }

    fun plusOne(){
        //数值为空时默认为0
        val count = _counter.value ?:0
        _counter.value = count + 1
    }

    fun clear(){
        _counter.value = 0
    }
}

map

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

比如,有一个User

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

可以在ViewModel中创建一个相应的LiveData来包含User类型的数据。
在这里插入图片描述
但如果MainActivity中明确只会显示用户的姓名,不关心用户的年龄,那么这个时候还将整个User类型的LiveData暴露给外部,就不那么合适了。
map()方法就是专门用于解决这种问题的,它可以将User类型的LiveData自由地转型成任意其他类型的LiveData
在这里插入图片描述
调用了Transformationsmap()方法来对LiveData的数据类型进行转换。map()方法接收两个参数:第一个参数是原始的LiveData对象;第二个参数是一个转换函数,在转换函数里编写具体的转换逻辑即可。这里的逻辑也很简单,就是将User对象转换成一个只包含用户姓名的字符串。
另外,将userLiveData声明成了private,以保证数据的封装性。外部使用的时候只要观察userName这个LiveData就可以了。当userLiveData的数据发生变化时,map()方法会监听到变化并执行转换函数中的逻辑,然后再将转换之后的数据通知给userName的观察者。

switchMap

如果ViewModel中的某个LiveData对象是调用另外的方法获取的,那么就可以借助switchMap()方法,将这个LiveData对象转换成另外一个可观察的LiveData对象。
因为如果调用 getUser()方法返回的都是一个新的LiveData实例,而上述写法会一直观察老的LiveData实例,从而根本无法观察到数据的变化。这种情况下的LiveData是不可观察的。

新建一个单例类Repository

object Repository {
    fun getUser(userId: String): LiveData<User>{
        val liveData = MutableLiveData<User>()
        liveData.value =User(userId, userId, 0)
        return liveData
    }
}

然后在MainViewModel中借助switchMap()方法,将这个LiveData对象转换成另外一个可观察的LiveData对象。在这里插入图片描述
这里定义了一个新的userIdLiveData对象,用来观察userId的数据变化,然后调用了TransformationsswitchMap()方法,用来对另一个可观察的LiveData对象进行转换。
为了测试代码是否可用,在activity_main.xml中添加一个idgetUserBtn的按钮,并在MainActivity中添加它的点击事件以及相关逻辑。

mainBinding.getUserBtn.setOnClickListener {
    val userId = (0..10000).random().toString()
    viewModel.getUser(userId)
} 

viewModel.user.observe(this, Observer { user ->
    mainBinding.infoText.text = user.firstName
})

这样在运行时多次点击按钮时,会发现屏幕上的数字一直随机变换。
即在按钮的点击事件中使用随机函数生成了一个userId,然后调用MainViewModelgetUser()方法来获取用户数据,但是这个方法现在不会有任何返回值了。等数据获取完成之后,可观察LiveData对象的observe()方法将会得到通知,在这里将获取的用户名显示到界面上。

流程梳理

首先,当外部调用MainViewModelgetUser()方法来获取用户数据时,并不会发起任何请求或者函数调用,只会将传入的userId值设置到userIdLiveData当中。
一旦userIdLiveData的数据发生变化,那么观察userIdLiveDataswitchMap()方法就会执行,并且调用我们编写的转换函数。
然后在转换函数中调用Repository.getUser()方法获取真正的用户数据。
同时,switchMap()方法会将Repository.getUser()方法返回的LiveData对象转换成一个可观察的LiveData对象,对于Activity而言,只要去观察这个LiveData对象就可以了。

Room

ORM(Object Relational Mapping)也叫对象关系映射。简单来讲,一般使用的编程语言是面向对象语言,而使用的数据库则是关系型数据库, 将面向对象的语言和面向关系的数据库之间建立一种映射关系,这就是ORM了。
ORM框架有一个强大的功能,就是可以用面向对象的思维来和数据库进行交互,绝大多数情况下不用再和SQL语句打交道了,同时也不用担心操作数据库的逻辑会让项目的整体代码变得混乱。
Room就是Android官方推出的一个ORM框架,并加入了Jetpack当中。

整体结构

Room主要由Entity、Dao和Database这3部分组成,每个部分都有明确的职责。

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

添加依赖

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

基础用法 - 增删改查

定义实体类 - Entity

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

这里只需修改之前User类中的代码

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

    @PrimaryKey(autoGenerate = true)
    var id: Long = 0
}

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

封装访问数据库操作 - Dao

访问数据库的操作无非就是增删改查这4种,但是业务需求却是千变万化的。而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 loadUsersOlder(age: Int): List<User>

    @Delete
    fun deleteUser(user: User)

    @Query("delete from User where lastName = :lastName")
    fun deleteUserByLastName(lastName: String): Int

}

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

  • insertUser()方法上面使用了@Insert注解,表示会将参数中传入的User对象插入数据库中,插入完成后还会将自动生成的主键id值返回。
  • updateUser()方法上面使用了@Update注解,表示会将参数中传入的User对象更新到数据库当中。
  • deleteUser()方法上面使用了@Delete注解,表示会将参数传入的User对象从数据库中删除。

以上几种数据库操作都是直接使用注解标识即可,不用编写SQL语句。

但是如果想要从数据库中查询数据,或者使用非实体类参数来增删改数据,那么就必须编写SQL语句了。比如UserDao接口中定义的loadAllUsers()方法,用于从数据库中查询所有的用户,如果只使用一个@Query注解,Room将无法知道想要查询哪些数据,因此必须在@Query注解中编写具体的SQL语句才行。

还可以将方法中传入的参数指定到SQL语句当中,比如loadUsersOlderThan()方法就可以查询所有年龄大于指定参数的用户。另外,如果是使用非实体类参数来增删改数据,那么也要编写SQL语句才行,而且这个时候不能使用@Insert@Delete@Update注解,而是都要使用@Query注解才行,参考
deleteUserByLastName()方法的写法。

定义Database - Database

这部分内容的写法是固定的,只需要定义好3个部分的内容:数据库的版本号、包含哪些实体类,以及提供Dao层的访问实例。

新建一个AppDatabase.kt文件

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

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

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

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

databaseBuilder()方法接收3个参数,注意第一个参数一定要使用
applicationContext,而不能使用普通的context,否则容易出现内存泄漏的情况,第二个参数是AppDatabaseClass类型,第三个参数是数据库名。最后调用build()方法完成构建,并将创建出来的实例赋值给instance变量,然后返回当前实例即可。

在Activity中实现逻辑

activity_main.xml中添加4个按钮,其id分别与对应的文字相同。
在这里插入图片描述

val userDao = AppDatabase.getDatabase(this).userDao()
val user1 = User("Tom","Brady",40)
val user2 = User("Tom","Hanks",63)

mainBinding.addDataBtn.setOnClickListener {
    thread {
        user1.id = userDao.insertUser(user1)
        user2.id = userDao.insertUser(user2)
    }
}

mainBinding.updateDataBtn.setOnClickListener {
    thread {
        user1.age = 42
        userDao.updateUser(user1)
    }
}

mainBinding.deleteDataBtn.setOnClickListener {
    thread {
        userDao.deleteUserByLastName("Hanks")
    }
}

mainBinding.queryDataBtn.setOnClickListener {
    thread {
       for (user in userDao.loadAllUsers()){
            Log.d("MainActivity.out",user.toString())
       }
    }
}

Add Data按钮的点击事件中,调用了UserDaoinsertUser()方法,将这两个User对象插入数据库中,并将insertUser()方法返回的主键id值赋值给原来的User对象。之所以要这么做,是因为使用@Update@Delete注解去更新和删除数据时都是基于这个id值来操作的。

结果展示

这样在测试时:

  • 红框:先点击Add Data按钮,再点击Query Data按钮,就展示最开始添加的user1user2
  • 黄框:先点击Update Data按钮,再点击Query Data按钮,发现user1中的年龄变成了42
  • 绿框:先点击delete Data按钮,再点击Query Data按钮,lastNamehanksuser被删掉了

在这里插入图片描述

注意

数据库操作属于耗时操作,Room默认是不允许在主线程中进行数据库操作的,因此上述代码将增删改查的功能都放到了子线程中。
不过为了方便测试,Room还提供了一个更加简单的方法,如下所示:

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

在构建AppDatabase实例的时候,加入一个allowMainThreadQueries()方法,这样Room就允许在主线程中进行数据库操作了。
这个方法建议只在测试环境下使用。

数据库升级

场景一:将数据库升级为2号版本,并添加一张Book表

首先要新建一个Book实体类

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

    @PrimaryKey(autoGenerate = true)
    var id: Long =0;

}

再创建它对应的BookDao接口

@Dao
interface BookDao {

    @Insert
    fun insertBook(book: Book): Long

    @Query("select * from Book")
    fun loadAllBooks(): List<Book>
    
}

然后就是在AppDatabase编写数据库升级的逻辑

@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

        @Synchronized
        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注解中,将版本号升级成了2,并将Book类添加到了实体类声明中,然后又提供了一个bookDao()方法用于获取BookDao的实例。
companion object结构体中,实现了一个Migration的匿名类,并传入了1和2这两个参数,表时当数据库版本从1升级到2的时候就执行这个匿名类中的升级逻辑。匿名类实例的变量命名也比较有讲究,这里命名成MIGRATION_1_2,可读性更高。
由于要新增一张Book表,所以需要在migrate()方法中编写相应的建表语句。另外必须注意的是,Book表的建表语句必须和Book实体类中声明的 结构完全⼀致,否则Room就会抛出异常
最后在构建AppDatabase实例的时候,加入一个addMigrations()方法,并把MIGRATION_1_2传入即可。 现在进行任何数据库操作时,Room就会自动根据当前数据库的版本号执行这些升级逻辑,从而让数据库始终保证是最新的版本。

场景二:将数据库升级为3号版本,并在表中添加新的一列

Book实体类中添加了作者字段时

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

    @PrimaryKey(autoGenerate = true)
    var id: Long =0;

}

对应的也要在AppDatabase中加入第2次升级的逻辑:
在这里插入图片描述

WorkManager

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

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

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

基本用法

在使用前要先添加依赖

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

WorkManager的基本用法主要分3步:

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

那么,首先要定义一个后台任务,这里创建一个SimpleWorker

class SimpleWorker(context: Context, params: WorkerParameters): Worker(context,params) {
    /**
     * 在这个方法中编写具体的后台任务
     * 它不会运行在主线程中,所以可以放心执行耗时任务
     * return 任务的执行结果 成功/失败
     */
    override fun doWork(): Result {
        Log.d("SimpleWorker","do work in SimpleWorker")
        return Result.success()
    }
}

然后在activity_main.xml中添加一个iddoWorkBtn的按钮,方便测试
MainActivity中完成最后两步

mainBinding.doWorkBtn.setOnClickListener {
    //配置后台任务的信息,这里是最简单的构建单次运行的后台任务请求
    val request = OneTimeWorkRequest.Builder(SimpleWorker::class.java).build()
    WorkManager.getInstance(this).enqueue(request)
}

在运行时点击按钮,就会弹出日志
在这里插入图片描述

处理复杂任务

延迟执行

在这里插入图片描述

添加标签

在这里插入图片描述
根据标签,就可以批量取消任务

WorkManager.getInstance(this).cancelAllWorkByTag("simple")

但如果没有设置标签,也可以通过id单个取消

WorkManager.getInstance(this).cancelWorkById(request.id)

也可以一次性取消所有后台任务请求

WorkManager.getInstance(this).cancelAllWork()

重新执行

如果任务返回的是Resut.retry(),可以通过红框中的语句重新执行
在这里插入图片描述

监听任务运行结果

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

链式任务

比如三个独立的后台任务要依次执行时

val work1 = ...
val work2 = ...
val work3 = ...
WorkManager.getInstance(this)
	.beginWith(work1)
	.then(work2)
	.then(work3)
	.enqueue()
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值