Jetpack 架构组件,带你快速上手
        
      
Jetpack
本文是我在学习Jetpack的过程中做的一些记录,如有错误,欢迎指正
本文包含了 ViewModel、Lifecycles、LiveData、Room、WorkManager 的相关用法,你可以通过目录直接跳转到你想了解的地方
ViewModel
简单介绍下 ViewModel:ViewModel 类旨在以注重生命周期的方式存储和管理界面相关的数据。ViewModel 类让数据可在发生屏幕旋转等配置更改后继续留存。
这是官网给出的介绍,简单解释一下 ViewModel 就是将界面 (Activity或Fragment) 中显示的数据从其中分离出来,单独进行处理,减少界面的逻辑复杂程度,减轻界面的负担。  
接下来简单使用一下:
添加依赖:
| 1 | implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0' | 
给对应的 Activity 创建一个对应的 XXXViewModel 类,并让它继承自 ViewModel (不管是 Activity 还是 Fragment 都最好给每一个都创建一个对应的 ViewModel )
| 1 | class MainViewModel : ViewModel(){ | 
在 Activity 中使用:
| 1 | class MainActivity : AppCompatActivity() { | 
现在我们实现了一个计数器,在这里需要注意,我们不能直接去创建 ViewModel 的实例,而是要通过 ViewModelProvider 来获取 ViewModel 的实例,之所以这样是因为 ViewModel 有独立的生命周期,且其生命周期长于 Activity 的生命周期,如果在 onCreat() 中创建 ViewModel 的实例,那么每次 onCreat() 执行时,ViewModel 都会创建一个实例,这样就无法保存其中的数据了。  
ViewModel 对象存在的时间范围是获取 ViewModel 时传递给 ViewModelProvider 的 Lifecycle。ViewModel 将一直留在内存中,直到限定其存在时间范围的 Lifecycle 永久消失,如下图是 Activity 的生命周期和 VIewModle 的生命周期对应的情况
        
      
向 ViewModel 传递参数
借助 ViewModelProvider.Factory ,下面我们来实现退出程序后再打开,数据仍然不会消失的效果
先修改 MainViewModel 的代码,如下:
| 1 | class MainViewModel(counterReserved: Int) : ViewModel(){ | 
接着创建 MainViewModelFactory 类,并实现 ViewModelProvider.Factory 接口,如下:
| 1 | class MainViewModelFactory(private val countReserved: Int) : ViewModelProvider.Factory { | 
我们这里实现了接口要求我们的 create 方法,在方法里面我们创建并返回了一个 MainViewModel 的实例,为什么我们这里就可以创建 MainViewModel 的实例了呢?因为 create() 方法的执行时机和 Activity 的生命周期无关,所以不会产生之前提到的问题。
最后修改 activity 中的代码,如下:
| 1 | class MainActivity : AppCompatActivity() { | 
Lifecycles
顾名思义,Lifecycles 是一个用来感知 Activity 生命周期的组件,下面来学习下简单用法。
简单使用:
新建一个 MyObserver 类,并实现 LifecycleObserver 接口
| 1 | class MyObserver : LifecycleObserver{ | 
LifecycleObserver 这是一个空方法接口,我们可以在 MyObserver 中定义任何方法,如果需要感知 Activity 的生命周期就需要为方法添加注解,如下所示:
| 1 | class MyObserver : LifecycleObserver{ | 
这里使用 @OnLifecycleEvent 注解,并传入了一种生命周期事件,生命周期事件一共有 7 种,分别是:ON_CREATE 、ON_START 、ON_RESUME 、ON_PAUSE 、ON_STOP 、ON_DESTROY 、ON_ANY 。前六种分别匹配 Activity 中相应的的生命周期回调,最后一种表示可以匹配 Activity 的任何生命周期回调。
接下来就是需要 LifecycleOwner 去通知 MyObserver 生命周期发生了变化,它可以使用如下的语法结构去通知 MyObserver
| 1 | lifecycleOwner.lifecycle.addObserver(MyObserver()) | 
这里 LifecycleOwner 调用了 getLifecycle 方法,得到一个 Lifecycle 对象,接着调用 addObserver 来观察 LifecycleOwner 的生命周期,再把 MyObserver 传进去。  
LifecycleOwner 是个什么?如何让获取一个 LifecycleOwner 的实例?
大多数情况下,只要 Activity 是继承自 AppCompatActivity 的,或者 Fragment 是继承自 androidx.fragment.app.Fragment 的,那么它们本身就是一个 LifecycleOwner 的实例,所以我们在 Activity 中就可以这样写
| 1 | class MainActivity : AppCompatActivity() { | 
现在程序可以感知到 Activity 的生命周期变化,但没法主动获知当前的生命周期状态,解决这个问题,只需要在 MyObserver 的构造函数中将 Lifecycle 对象传进去即可,如下:
| 1 | class MyObserver(val lifecycle: Lifecycle) : LifecycleObserver{......} | 
有了 Lifecycle 对象后,就可以在任何地方调用 lifecycle.currntState 来主动获取当前的生命周期状态。lifecycle.currntState 返回的生命周期状态时一个枚举类型,一共有 5 种状态类型,如下:
| 1 | INITIALIZED | 
它们与 Activity 的生命周期回调所对应的关系如图:
        
      
LiveData
LiveData 是一种可观察的数据存储器类。与常规的可观察类不同,LiveData 具有生命周期感知能力,意指它遵循其他应用组件(如 Activity、Fragment 或 Service)的生命周期。这种感知能力可确保 LiveData 仅更新处于活跃生命周期状态的应用组件观察者。
简单使用
LiveData 可以包含任何类型的数据,并在数据发生变法的时候通知给观察者  
修改 MainViewModel 中的代码,如下:
| 1 | class MainViewModel(countReserved: Int) : ViewModel() { | 
这里将 counter 变量修改成了一个 MutableLiveData 对象,这是一种可变的 LiveData 。它主要有三种读写数据的方法,分别是:  
- getvalue()//用于获取- LiveData中包含的数据
- setValue()//用于给- LiveData设置数据,但是只能在主线程中调用
- postValue()//用于在非主线程中给- LiveData设置数据
下面来修改 MainActivity 中的代码:
| 1 | class MainActivity : AppCompatActivity() { | 
这里 counter 变量已经变成了一个 LiveData 对象,任何 LiveData 对象都可以调用它的 observe() 方法来观察数据的变化。observer() 方法接收两个参数:第一个参数是一个 LifecycleOwner 对象,这里也就是 Activity 自己。第二个参数是一个 Observer 接口,当 counter 中包含的数据发生变化时,就会回调到这里。  
关于 observe() 方法,Google 官方在专门面向 Kotlin 语言的 API 中提供了很多好用的语法扩展,要使用它需添加依赖:
| 1 | implementation 'androidx.lifecycle:lifecycle-livedata-ktv:2.2.0' | 
之后我们就可以使用如下结构的 observe() 方法了
| 1 | viewModel.counter.observe(this) { count -> | 
以上是 LiveData 基本用法,可以正常使用,但仍然不是最规范的用法, 主要问题是我们将 counter 这个可变的 LiveData 暴露给了外部,这样在 ViewModel 外面也是可以给 counter 设置数据,从而破坏了 LiveData 数据的封装性  
比较推荐的做法是,永远只暴露不可变的 LiveData 给外部,下面来改造下 MainViewModel ,如下:
| 1 | class MainViewModel(countReserved: Int) : ViewModel() { | 
这里 _counter 变量对于外部便是不可见的,而我们又定义了一个 counter 变量,类型声明为不可变的 LiveData ,并在它的 get() 属性方法中返回 _counter 变量。
这样当外界调用 counter 变量时,实际上获得的是 _counter 的实例,但是无法给 counter 设置数据,从而保证了 LiveData 数据的封装性  
LiveData 中的 map 和 switchMap
LiveData 为了能够应对各种不同需求场景,提供了两种转换方法:map() 和 switchMap() 方法
map()
这个方法的作用是将实际包含数据的 LiveData 和仅用于观察数据的 LiveData 进行转换,实例如下:
比如有一个 User 类,其中包含用户的姓名、年龄,如下:
| 1 | data class User(var firstName: String,var lastName:String, var age: Int) {} | 
这里包含了三个数据,但如果我们的 Activity 明确了只需要用户的姓名,不关心年龄时 还将整个 User 暴露出去就不太合适,这时候就可以使用 map() 方法,将 User 类型的 LiveData 自由的转型成任意其他类型的 LiveData,如下:
| 1 | class MainViewModel(countReserved: Int) : ViewModel() { | 
map() 方法接收两个参数:
- 第一个:参数是原始的 LiveData对象;
- 第二个:参数是一个转换函数
我们就只需要在转换函数里写具体的逻辑即可
另外,我们将 userLiveData 声明成了 private ,以保证数据的封装性,外部只需要观察 userName 就可以了,当 userLiveData 数据发生变化时,map() 方法会监听到变化并执行转换函数中的逻辑,然后将转换之后的数据通知给 userName 的观察者
switchMap()
我们通过一个实例来学习。根据传入的 userId 参数去服务器请求或者到是数据库中查找相应的 User 对象,但是这里只是模拟示例,因此每次传入的 userId 当作用户姓名来创建一个新的 User 对象即可  
代码如下,先创建一个 Repository 单例类,模拟获取用户数据的功能:
| 1 | object Repository { | 
下面获取 Repository 的 LiveData 对象,
| 1 | class MainViewModel(countReserved: Int) : Int{ | 
在 Activity 中观察 LiveData
| 1 | viewModel.getUser(userId).observe(this) { -> | 
以上的这种呢做法完全错误,因为每次调用 getUser() 方法返回的都是一个新的 LiveData 实例,而上述写法会一直观察老的 LiveData 实例,这种情况下,LiveData 是不可能观察的   
以下是正确做法,
借助 switchMap ,它的使用场景比较固定:如果 VIewModel 中的某个 LiveData 对象是调用另外的方法获取的,那么就可以借助 switchMap() 方法,将这里 LiveData 对象转换成另外一个可观察的 LiveData 对象。修改 MainViewModel 中的代码,如下:
| 1 | class MainViewModel(countReserved: Int) : ViewModel() { | 
switchMap() 方法接收两个参数:  
- 第一个:传入新增的 userIdLiveData,switchMap()方法会对它进行观察
- 第二个:是一个转换函数,我们必须在这个转换函数中返回一个 LiveData对象,因为switchMap()方法的工作原理就是要将转换函数中返回的LiveData对象转换成另一个可观察的LiveData对象。
现在 user 对象就是一个可观察的 LiveData 对象了
修改 Activity 中的代码,如下:
| 1 | class MainActivity : AppCompatActivity() { | 
现在已经实现了功能并且可以正常运行了。
当我们的 ViewModel 中某个获取数据的方法有可能是没有参数的,这个时候该怎么办呢?
这里我们先创建一个空的 LiveData 对象:
| 1 | class MyViewModel : ViewModel(){ | 
在 refresh() 方法中,只是将 refreshLiveData 原有的数据取出来(默认是空),再重新设置到 refreshResult 当中,这样就能触发一次数据变化。
在 LiveData 内部不会判断即将设置的数据和原有数据是否相同,只要调用了 setValue() 或 postValue() 方法,就一定会触发数据变化事件,然后我们只需要在 Activity 中观察 refreshResult 这个 LiveData 对象即可
Room
Room 是 Google 官方推出的一个 ORM(Object Relational Mapping 对象关系映射)框架,并将它加入了 Jetpack 中。
Room 的整体结构主要由 Entity、Dao、Database 这三个部分组成,每个部分都有自己明确的职责:
- Entity:用于定义封装实际数据的实体类,每个实体类都会在数据库中都有一张对应的表,并且表中的列是根据实体类中的字段自动生成的。
- Dao:- Dao是数据访问的意思,同长会在这里对数据库的各项操作进行封装。在实际编程中,逻辑层就不用和底层数据打交道了,直接和- Dao层进行交互就可。
- Database:用于定义数据库中的关键信息,包括数据库的版本号、包含哪些实体类以及提供- Dao层的访问实例。
Room 的具体用法
添加依赖:
| 1 | .... | 
kapt 只能在 Kotlin 项目中使用,如果是 Java 项目的话,使用 amotationProcessor 即可
接下来按照刚才介绍的 Room 的三个部分来一一进行实现
定义 Entity:
我们直接就使用 上文定义的 User 类来改造
| 1 | 
 | 
@Entity 注解:将 User 声明成了一个实体类@PrimaryKey 注解:将 id 字段设为主键,将 autoGenerate 设置为 true ,使得主键的值自动生成
定义 Dao:
这一部分比较关键,因为所有访问数据库的操作都是在这里封装
新建一个 UserDao 接口,如下:
| 1 | 
 | 
@Dao 注解:让 Room 能够识别到 UserDao 是一个 Dao@Insert 注解:表示将参数传入 User 对象插入数据库中,插入完成后还会返回自动生成的主键 id 值@Update 注解:表示会将参数中传入的 User 对象更新到数据库当中@Delete 注解:表示会将参数传入的 User 对象从数据库中删除
以上几种数据库操作都直接使用注解表示即可,不用编写 SQL 语句。如果想要从数据库中查询数据,或者使用非实体类参数来增删改查数据,就必须编写 SQL 语句,比如刚刚定义的 loadAllUsers() 方法,用于从数据库中查询所有用户。
@Query 注解:该注解中必须编写 SQL 语句,可以将方法中传入的参数指定到 SQL 语句当中,比如 loadUserOlderThan() 方法就可以查询所有年龄大于指定参数的用户。 
如果是使用非实体类参数来增删改查数据,也要编写 SQL 语句才行,而且只能使用 @Query 注解,比如 deleteUserByLastName() 方法
定义 Database
这部分一般只需要定义三个部分:数据库版本号、包含哪些实体类、提供 Dao 层的访问实例。
新建一个 AppDatabase.kt 文件,如下:
| 1 | 
 | 
@Database 注解:其中声明了数据库版本号以及包含的实体类,多个实体类之间用逗号隔开。AppDatabase 类必须继承自 RoomDatabase 类,并且一定要使用 abstract 关键字声明成抽象类,然后提供相应的抽象方法,用于获取之前的 Dao 实例,比如这里提供的 userDao() 方法。之后在 companion object 结构体中编写了一个单例模式,原则上全局应该只存在一个 Appdatabase 实例。  
databaseBuilder() 方法接收三个参数:
- 第一个:参数一定要使用 applicationContext,而不能使用普通context(),否则容易出现内存泄漏的情况。
- 第二个:参数是 AppDatabase的Class类型。
- 第三个:参数是数据库名
修改 MainActivity 中的代码:
| 1 | class MainActivity : AppCompatActivity() { | 
在 AddData 的点击事件中将 insertUser() 方法返回的主键 id 值赋值给了原来的 User 对象。之所以这样是因为使用 @Update、@Delete 注解去更新和删除数据时都是基于这个 id 值来操作的。
由于数据库操作属于耗时操作,Room 默认不允许在主线程中进行数据库操作,因此上述的增删改查操作都放在了子线程中,不过为了方便调试,Room 还提供了一个更加简单的方法:
| 1 | Room.databaseBuilder(context.applicationContext, AppDatabase::class.java, "app_database") | 
这样 Room 就允许在主线程进行数据库操作了,不过建议只在测试环境使用
Room 的数据库升级
Room 在数据库升级方面设计得比价繁琐,并没有比原生的 SQLiteDatabase 简单到哪儿去。如果你目前还只是在开发测试阶段,不想编写那么繁琐的数据库升级逻辑,Room 提供了一个简单粗暴的方法:
| 1 | Room.databaseBuilder(aontext.applicationContext, AppDatabase::class.java. "app_database") | 
构建 AppDatabase 实例时加入了一个 fallbackToDestructiveMigration() 方法,这样只要数据库进行了升级,Room 就会将当前的数据库销毁,然后再重新创建,但这样做的问题也就显而易见,之前数据库中的数据也会全部丢失。
下面是正规用法
我们先新建一个 Book 的实体类:
| 1 | 
 | 
然后创建一个 BookDao 接口,并在其中随意定义一些 API
| 1 | 
 | 
修改 AppDatabase 中的代码:
| 1 | 
 | 
我们在 @Database 注释中将版本号升级到了 2 ,并将 Book 嘞添加到了实体类声明中。
在companion object 结构体中,实现了一个 Migration 的匿名类,并传入了 1 和 2 这两个参数,表示当前数据库版本从 1 升级到 2 的时候就执行这个匿名类中的升级逻辑。这里是要新增一张 Book 表,所以需要在 migrate() 方法中编写相应的建表语句。Book 表的建表语句必须和 Book 实体类中声明的结构完全一致。最后在构建 AppDatabase 实例时,加入一个 addMigrations() 方法,并将参数传入即可。  
当我们的数据库可能不需要新建表,而是添加一个列时,可以使用 alter 语句修改表结构即可,如下:
| 1 | 
 | 
这里添加了一个页数 pages 的字段,之后修改 AppDatabase 的代码:
| 1 | 
 | 
升级步骤和之前差不多,就不多说了
WorkManager
WorkManager 适合用于处理一些要求定时执行的任务,它可以根据操作系统的版本自动选择底层是使用 AlarmManager 实现还是 JobScheduler 实现,而且它还支持周期性任务、链式任务处理等功能。  
WorkManager 的基本用法主要分以下三步:  
- 定义一个后台任务,并实现具体的任务逻辑;
- 配置该后台任务的运行条件和约束信息,并构建后台任务请求;
- 将改后台任务请求传入 WorkManager的enqueue()方法中,系统会在合适的时间运行。
下面来按照以上步骤来实现。第一步,定义一个后台任务,新建一个 SimpleWorker 类,如下:
| 1 | class SimpleWorker(context: Context, params: WorkerParameters) : Worker(context,params) { | 
每一个后台任务都必须继承 Work 类。doWork() 方法不会运行在主线程中,因此可以在这里执行耗时操作,另外该方法要求返回一个 Result 对象,用于表示任务的运行结果。成功就是 Result.success() ,失败是 Result.failure() 。除了这两种还有一个 Result.retry() 方法,它其实也代表着失败,只是可以结合 WorkRequest.Builder 的 setBackoffCriteria() 方法来重新执行任务。
第二步,配置该后台任务的运行条件和约束信息。
这里进行最基本的配置,代码如下:
| 1 | var request= OneTimeWorkRequest.Builder(SimpleWorker::class.java).build() | 
OneTimeWorkRequest.Builder 是 WorkRequest.Builder 的子类,用于构建单词运行的后台任务请求,还有另一个 PeriodicWorkRequest.Builder 也是其子类,用于构建周期性运行的任务请求,但为了降低设备性能消耗,它的构造函数中传入的运行周期间隔不能短于 15 分钟,实例如下:
| 1 | var request = PeriodicWorkRequest.Builder(SimpleWorker::class.java, 15, | 
第三步,最后只需要将构建好的后台任务请求传入 WorkManager 的 enqueue() 方法中,系统就会在合适的时间去运行了:
| 1 | WorkManager.getInstance(context).enqueue(request) | 
下面来测试下,修改 MainActivity 中的代码:
| 1 | class MainActivity : AppCompatActivity() { | 
这里后台任务的具体时间由我们所指定的约束以及系统的一些优化所决定的,由于这里没有指定任何约束,因此后台任务基本上会在点击按钮之后立刻运行。
使用 WorkManager 处理复杂任务
让后台任务在指定的延迟时间后运行,可以借助 setInitialDelay() 方法,如下:
| 1 | var request = PeriodicWorkRequest.Builder(SimpleWorker::class.java) | 
setInitalDelay() 接收两个参数,第一个是要延迟的时间,第二个是该时间的单位:可以选择毫秒、秒、分钟、小时、天都可以。
可以控制运行时间之后,再来添加点别的功能,比如给后台任务请求添加标签,如下所示:
| 1 | var request = PeriodicWorkRequest.Builder(SimpleWorker::class.java) | 
该功能最主要的一个功能就是通过标签来取消后台任务请求:
| 1 | WorkManager.getInstance(this).cancelAllWorkByTag("simple") | 
使用标签可以将可以将同一标签名的所有后台请求全部取消
如果没有标签,也可以通过 id 来来取消后台任务请求:
| 1 | WorkManager.getInstance(this).cancelAllWorkById(request.id) | 
如果想要一次性取消所有后台任务:
| 1 | WorkManager.getInstance(this).cancelAllWork() | 
在上文中提到,如果 doWork() 方法中返回了 Result.retry(),那么可以结合 setBackoffCriteria() 方法来重新执行任务,如下:
| 1 | var request = PeriodicWorkRequest.Builder(SimpleWorker::class.java) | 
setBackoffCriteria() 接收三个参数:
- 第一个:参数用于指定如果任务再次执行失败,下次重试的时间应该以什么样的形式延迟。该参数可选值有两种:- LINEAR:表示下次重试时间以线性的方式延迟。
- EXPONENTIAL:表示下次重试的时间以指数的方式延迟。
 
- 第二个和第三个都用于指定在多久之后重新执行任务,时间最短不能少于 10 秒钟
接下来我们也可以对 Result.success() 和 Result.failure() 任务结果进行监听,如下:
| 1 | WorkManager.getInstance(this) | 
这里调用了 getWorkInfoByIdLiveData() 方法,并传入后台任务请求的 id ,会返回一个 LiveData 对象,接着就可以调用 LiveData 对象的 observe() 方法来观察数据变化了,以此监听后台任务的运行结果。
另外,调用 getWorkInfoByTagLiveData() 方法也可以监听同一标签名下所有后台任务请求的运行结果,用法差不多。
链式任务
假设定义了 3 个独立的后台任务:同步数据、压缩数据、上传数据,现在要实现先同步、再压缩、对吼上传的功能,就可以借助链式任务来实现,如下:
| 1 | val sync = .... | 
beginWith() 方法用于开启一个链式任务,后面要执行的任务只需要使用 then() 方法来连接即可。
另外,WorkManager 还要求,必须在前一个后台任务运行成功之后,下一个后台任务才会运行,也就是说,如果某一个任务运行失败、或者取消了,之后的任务就都不能运行了
以上介绍的 WorkManager 的所有功能,在国产手机上都有可能得不到正确的运行,因为绝大多数的国产手机厂商在进行 Android 系统定制时会增加一个一键关闭的功能,允许用户一键杀死所有非白名单的应用程序,而被杀死的应用程序既无法接收广播,也无法运行 WorkManager 的后台任务,所有千万别依赖 WorkManager 去实现什么核心功能,因为它在国产手机上可能会非常不稳定
更多文章:
Kotlin学习知识点整理-基础篇-01
Kotlin学习知识点整理-基础篇-02
Kotlin学习知识点整理-基础篇-03
Kotlin学习知识点整理-进阶篇-04
写在最后唠唠《一日重生》佳句分享
Going back to something is harder than you think.
回到过去,比你想象的更难。