13-Jetpack

Jetpack

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

全家桶: 基础, 架构, 行为, 界面; 这里我们主要介绍 Jetpack 架构

MMVM

MMVM, Model-View-ViewModel, Model 是数据模型部分;View 是界面展示部分;ViewModel 可以理解成一个连接数据模型和界面展示的桥梁,从而实现让业务逻辑和界面展示分离的程序结构设计

  • UI 控制层: Activity, Fragment, 布局文件等与界面相关的东西
  • ViewModel: 层用于持有和 UI 元素相关的数据,以保证这些数据在屏幕旋转时不会丢失; 提供接口给 UI 控制层调用以及和仓库层进行通信
  • 仓库: 判断调用方请求的数据应该是从本地数据源中获取还是从网络数据源中获取,并将获取到的数据返回给调用方
    • 本地数据源: 数据库、SharedPreferences 等持久化技术
    • 网络数据源: 使用 Retrofit 访问服务器提供的 Webservice 接口来实现

ViewModel

Motivation: 传统 Activity 既要负责逻辑处理又要负责 UI 展示, 甚至还得处理网络回调

ViewModel 是专门用于存放与界面相关的数据的

  • 手机发生横竖屏旋转时, Activity 会被重建, 但 ViewModel 声明周期与 Activity 不同, 因此数据也不会丢失
  • 只有调用 onCleared() 方法 ViewModel 才会被销毁

Quick Start

  1. 推荐给每一个 Activity 和 Fragment 都创建一个对应的 ViewModel, 里面存储用于展示的信息
class MainViewModel: ViewModel() {
var counter = 0
}
  1. 在对应的 Activity 或 Fragment 中创建对应的 viewModel 实例
val viewModel by lazy { ViewModelProvider(this).get(MainViewModel::class.java) }
  1. 逻辑操作
1
2
3
4
5
val buttonPlus: Button = findViewById(R.id.plusOneBtn)
buttonPlus.setOnClickListener {
viewModel.counter++
refreshCounter()
}

向 ViewModel 传递参数

比如让 ViewModel 加载的时候, 可以先在 Activity 中尝试读取本地缓存的数据信息, 然后将读取的内容传递给 ViewModel 进行数据维护

  1. 需要创建一个 ViewModel 的工厂类, 重写其 create 方法, 使其适配参数传递
1
2
3
4
5
6
7
class MainViewModelFactory(private val countReserved: Int): ViewModelProvider.Factory {

override fun <T : ViewModel?> create(modelClass: Class<T>): T {
return MainViewModel(countReserved) as T
}

}
  1. 使用工厂类实现 ViewModel 的实例化
viewModel = ViewModelProvider(this, MainViewModelFactory(countReserved)).get(MainViewModel::class.java)

Lifecycles

Motivation: 对 Activity 生命周期情况进行感知, 某个界面中发起了一条网络请求,但是当请求得到响应的时候,界面或许已经关闭了,这个时候就不应该继续对响应的结果进行处理。因此,我们需要能够时刻感知到Activity的生命周期,以便在适当的时候进行相应的逻辑控制。

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

Quick Start

  1. 新建一个自己的 MyObserver 类,并让它实现 LifecycleObserver 接口(主要就是声明一下)
class MyObserver: LifecycleObserver {}
  1. 接口中根据相关注解定义感知 Activity 生命周期的逻辑处理方法

声明周期事件类型: ON_CREATE, ON_START, ON_RESUME, ON_PAUSE, ON_STOP, ON_DESTROY; 特殊的有一种 ON_ANY 表示可以匹配 Activity 的任何生命周期回调

1
2
3
4
5
6
7
8
9
10
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")
}
}
  1. 获取 Activity 或 Fragment 本身的 LifecycleOwner 实例, 实现对 MyObserver 的通知
lifecycle.addObserver(MyObserver())

主动状态感知

在 Observer 中主动获知当前的生命周期状态

  1. 构造 Observer 时需要将 Lifecycle 对象传递进来
class MyObserver(val lifecycle: Lifecycle) : LifecycleObserver {
...
}
  1. 通过 lifecycler 对象获取当前 Activity 的状态
lifecycle.currentState
// 返回的生命周期状态是一个枚举类型: INITIALIZED, DESTROYED, CREATED, STARTED, RESUMED

LiveData

Motivation: 对 ViewModel 进行写后读时, 在单线程模式没问题, 但是如果复杂写, 需要开启额外线程执行, 那么写后读, 读到的大概率还是旧数据

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

  • 常与 ViewModel 结合使用
  • 在子线程中给 LiveData 设置数据,要调用 postValue() 方法,而不能再使用 setValue() 方法

Quick Start

  1. 使用相关的 LiveData 去维护相关变量
var counter = MutableLiveData<Int>()
  1. 初始化变量, 并书写变量相关的处理接口
1
2
3
4
5
6
7
8
9
10
11
12
init {
counter.value = countReserved
}

fun plusOne() {
val count = counter.value ?: 0
counter.value = count + 1
}

fun clear() {
counter.value = 0
}
  1. 在 Activity 中书写相关逻辑处理, 并监听回调

observe 方法接收两个参数:第一个参数是 LifecycleOwner 对象(Activity OR Fragment);第二个参数是一个 Observer 接口,表示当 counter 中包含的数据发生变化时,就会回调到这里

1
2
3
4
5
6
7
8
9
10
11
12
13
val buttonPlus: Button = findViewById(R.id.plusOneBtn)
buttonPlus.setOnClickListener {
viewModel.plusOne()
}

val buttonClean: Button = findViewById(R.id.clearBtn)
buttonClean.setOnClickListener {
viewModel.clear()
}

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

更好的封装性

Quick Start 中将 counter 这个可变的 LiveData 暴露给了外部,这样即使是在 ViewModel 的外面也是可以给 counter 设置数据的,从而破坏了 ViewModel 数据的封装性,同时也可能带来一定的风险

因此永远只暴露不可变的 LiveData 给外部,这样在非 ViewModel 中就只能观察 LiveData 的数据变化,而不能给 LiveData 设置数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

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

// var counter = MutableLiveData<Int>()

// counter 变量, 类型为不可变的 LiveData, 重写 get 方法返回 _counter 变量
// 当外部调用 counter 变量时,实际上获得的就是 _counter 的实例(转型为 LiveData 类型),但是无法给 counter 设置数据,从而保证了 ViewModel 的数据封装性
val counter: LiveData<Int>
get() = _counter

// private 修饰, 对外部不可见
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
}
}

map

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

Application: ViewModel 保存了一个用户数据类的实例, 但是界面展示时只会涉及到用户的名字, 不会涉及到其年龄, 身高, 因此将整个用户数据实例暴露给外部读取也十分危险, 需要进一步封装转换, 使其只暴露姓名

1
2
3
4
private val userLiveData = MutableLiveData<User>()
val username: LiveData<String> = Transformations.map(userLiveData) {
user -> "${user.firstName} ${user.lastName}"
}

switchMap

前面我们所学的所有内容都有一个前提:LiveData 对象的实例都是在 ViewModel 中创建的

然而在实际的项目中,不可能一直是这种理想情况,很有可能 ViewModel 中的某个 LiveData 对象是调用另外的方法获取的

Quick Start

  1. 通过数据库获取所有的 TodoItem
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
fun refreshTodoItemByCategory(todoCategoryId: Long) = fire(Dispatchers.IO) {
if(todoCategoryId == -1L) {
val todoItemList = todoDatabase.todoItemDao().getAllTodoItem()
Result.success(Pair("星海", todoItemList))
} else {
// 获取类别名词 - 用于标题栏展示
coroutineScope {
val todoCategory_ = async {
todoDatabase.todoCategoryDao().getTodoCategoryById(todoCategoryId)
}
val todoItemList_ = async {
todoDatabase.todoItemDao().getTodoItemsByCategoryId(todoCategoryId)
}
val todoCategoryName: String = todoCategory_.await().name
val todoItemList = todoItemList_.await()
Result.success(Pair(todoCategoryName, todoItemList))
}
}
}
// fire: 统一的入口函数中进行封装, 使得只要进行一次 try catch 处理就行了
// 自动开启线程处理, 根据成功或失败返回相应的 Result 对象
private fun <T> fire(
context: CoroutineContext,
block: suspend () -> Result<T>
) = liveData<Result<T>>(context){
val result = try {
block()
} catch (e: Exception) {
Result.failure<T>(e)
}
emit(result)
}
  1. 维护内部观察者, 触发数据库操作, 并返回 LiveData 对象
1
2
3
4
5
6
7
8
9
10
11
12
// 维护 TodoCategoryId, 如果其变化则触发数据库 refreshTodoItemByCategory 操作
private val refreshTodoItemByCategoryObs = MutableLiveData<Long>()

val refreshTodoItemByCategoryResult = Transformations.switchMap(refreshTodoItemByCategoryObs) { todoCategoryId ->
// 返回 Result 对象, 交由外部 refreshTodoItemByCategoryResult 的 Observer 进行处理
Repository.refreshTodoItemByCategory(todoCategoryId)
}

// 提供给外部的事件调用接口, 使得监听数据产生变化, 进而触发数据库操作
fun refreshTodoItemByCategory(todoCategoryId: Long) {
refreshTodoItemByCategoryObs.value = todoCategoryId
}
  1. 维护外部观察者, 对数据库返回的数据加工处理
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
viewModel.refreshTodoItemByCategoryResult.observe(this, Observer { result ->
val pairResult = result.getOrNull()
if(pairResult != null) {
val todoCategoryName = pairResult.first
val todoItemList = pairResult.second

viewModel.todoItemList.clear()
viewModel.todoItemList.addAll(todoItemList)
adapter_todoitem.notifyDataSetChanged()

toolbarFragment.refreshToolbarName(todoCategoryName, viewModel.todoCategoryId == -1L)

if(todoItemSwipeRefresh.isRefreshing) {
todoItemSwipeRefresh.isRefreshing = false
}

if(drawerLayout.isDrawerOpen(GravityCompat.START)) {
drawerLayout.closeDrawer(GravityCompat.START)
}
}
})

Tips: 如果外部生成的 LiveData 实例没有相关属性可供监听, 那么只需要监听一个空属性即可

1
2
3
4
5
6
7
8
private val refreshLiveData = MutableLiveData<Any?>()
val refreshResult = Transformations.switchMap(refreshLiveData) {
Repository.refresh() // 假设Repository中已经定义了refresh()方法
}
// LiveData 内部不会判断即将设置的数据和原有数据是否相同,只要调用了 setValue() 或 postValue() 方法,就一定会触发数据变化事件
fun refresh() {
refreshLiveData.value = refreshLiveData.value
}

ROOM

ROOM 是为 Android 数据库设计的 ORM 框架, 是对 SQLite 的封装; Room provides an abstraction layer over SQLite to allow fluent database access while harnessing the full power of SQLite

  • Object Relational Mapping, 将面向对象的语言和面向关系的数据库之间建立一种映射关系

Overall Structure: Entity, Dao, Database

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

Strength

  • 用面向对象的思维来和数据库进行交互
  • Room 支持在编译时动态检查 SQL 语句语法
  • Compile-time verification of SQL queries. each @Query and @Entity is checked at the compile time

Quick CRUD

常用操作: Create, Read, Update, Delete
常用注解:

  • @Entity(foreignKeys, indices, primaryKeys, tableName)
  • @PrimaryKey(autoGenerate = true)
  • @ColumnInfo(name=”column_name”): allows specifying custom information about column
  • @Ignore: field will not be persisted by Room
  • @Embeded: nested fields can be referenced directly in the SQL queries
  1. 引入相关依赖
  • kotlin-kapt 插件: 引入 Room 编译时的注解库
1
2
3
4
5
6
7
8
9
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"
}
  1. 编写相关实体类, 使用 ROOM 的注解完成面向对象
1
2
3
4
5
6
7
// 定义实体类的注解
@Entity
data class User(var firstName: String, var lastName: String, var age: Int) {

@PrimaryKey(autoGenerate = true)
var id: Long = 0
}
  1. 编写相关的数据库操作接口, DAO
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
@Dao
interface UserDao {

// 表示会将参数中传入的 User 对象插入数据库中,插入完成后还会将自动生成的主键 id 值返回
@Insert
fun insertUser(user: User): Long

// 会将参数中传入的 User 对象更新到数据库当中
@Update
fun updateUser(newUser: User)

// 会将参数传入的 User 对象从数据库中删除
@Delete
fun deleteUser(user: User)

// 查询或精细化的删除操作, 需要传入相关的 SQL 语言
@Query("select * from User")
fun findAllUsers(): List<User>

@Query("select * from User where age > :age")
fun findUsersOlderThan(age: Int): List<User>

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

}
  1. 定义 Database: 数据库的版本号、包含哪些实体类、提供 Dao 层的访问实例

WorkManager

WorkManager 是一个处理定时任务的工具,可以保证即使在应用退出甚至手机重启的情况下,之前注册的任务仍然将会得到执行

  • 但是为了减少电量消耗,进行了一些优化,因此不能保证准时执行;比如将触发时间临近的几个任务放在一起执行,这样可以大幅度地减少 CPU 被唤醒的次数,从而有效延长电池的使用时间

Quick Start

  1. 添加相关依赖: implementation "androidx.work:work-runtime:2.7.1"
  2. 定义一个后台任务,并实现具体的任务逻辑
1
2
3
4
5
// 不会运行在主线程
override fun doWork(): Result {
Log.d("SimpleWorker", "do work in jcworker")
return Result.success() // Result.failure() OR Result.retry()
}
  1. 配置该后台任务的运行条件和约束信息,并构建后台任务请求
1
2
3
4
5
6
7
8
// Example ////////////////////
val request = OneTimeWorkRequest.Builder(SimpleWorker::class.java).build() // basic
val request = PeriodicWorkRequest.Builder(SimpleWorker::class.java, 15, TimeUnit.MINUTES).build() // Periodic
// Basic /////////////////////
//// 后台任务的具体运行时间是由我们所指定的约束以及系统自身的一些优化所决定
//// 由于这里没有指定任何约束,因此后台任务基本上会在点击按钮之后立刻运行
val request = OneTimeWorkRequest.Builder(JCWorker::class.java).build()
WorkManager.getInstance(this).enqueue(request)
  1. 将该后台任务请求传入 WorkManager 的 enqueue() 方法中,系统会在合适的时间运行
WorkManager.getInstance(context).enqueue(request) // basic

延时任务

构建后台任务请求时借助 setInitialalDelay() 方法让后台任务延时执行

  • 单位: TimeUnit.SECONDS, MINUTES, HOURS, DAYS, MICROSECONDS, MILLISECONDS, NANOSECONDS
1
2
3
4
// 推迟 5s 执行
val request = OneTimeWorkRequest.Builder(JCWorker::class.java)
.setInitialDelay(5, TimeUnit.SECONDS)
.build()

取消后台任务

  1. 可以在后台任务构建时指定该任务的标签, 多个任务可以绑定相同标签, 从而进行统一控制
1
2
3
4
val request = OneTimeWorkRequest.Builder(JCWorker::class.java)
.setInitialDelay(5, TimeUnit.SECONDS)
.addTag("JCWork")
.build()
  1. 使用 WorkManager 的实例方法进行取消
WorkManager.getInstance(this).cancelAllWorkByTag("JCWork")
// 或者使用 id 取消
WorkManager.getInstance(this).cancelWorkById(request.id)

任务执行状态的监听

Result.retry()

如果在复现的 doWork() 方法中返回了 Result.retry() 那么可以提前通过 setBackoffCriteria() 方法配置好任务的重启配置

Tips: 如果任务一直执行失败, 那么一直重试没有意义, 因此应让重试间隔随着重试次数的增加而增加

  • BackoffPolicy.LINEAR: 代表下次重试时间以线性的方式延迟
  • BackoffPolicy.EXPONENTIAL 代表下次重试时间以指数的方式延迟
1
2
3
4
5
6
7
// 第一个参数则用于指定如果任务再次执行失败,下次重试的时间应该以什么样的形式延迟
// 第二, 三个参数用于指定在多久之后重新执行任务,时间最短不能少于 10 秒钟
val request = OneTimeWorkRequest.Builder(JCWorker::class.java)
.setInitialDelay(5, TimeUnit.SECONDS)
.addTag("JCWork")
.setBackoffCriteria(BackoffPolicy.LINEAR, 10, TimeUnit.SECONDS)
.build()

SUCCEEDED OR FAILED

成功或失败的执行状态需要通过对 WorkManager 的配置进行响应

1
2
3
4
5
6
7
8
9
// 返回一个 LiveData 对象, 用这个维护任务的状态
WorkManager.getInstance(this).getWorkInfoByIdLiveData(request.id)
.observe(this) { workInfo ->
if (workInfo.state == WorkInfo.State.SUCCEEDED) {
Log.d("SimpleWorker", "Succeeded")
} else if (workInfo.state == WorkInfo.State.FAILED) {
Log.d("SimpleWorker", "FAILED")
}
}

链式任务

定义了多个独立的任务且具有先后顺序的情况, 使用链式任务进行先后执行

Tips: 必须在前一个后台任务运行成功之后,下一个后台任务才会运行。也就是说,如果某个后台任务运行失败,或者被取消了,那么接下来的后台任务就都得不到运行了

1
2
3
4
5
6
7
8
val sync = ... // 同步数据任务
val compress = ... // 压缩数据任务
val upload = ... // 上传数据任务
WorkManager.getInstance(this)
.beginWith(sync)
.then(compress)
.then(upload)
.enqueue()

额外注意

前面所介绍的 WorkManager 的所有功能,在国产手机上都有可能得不到正确的运行

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

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