探究Jetpack

Jetpack

QQ_1737786500747

以上是MVVM架构,Jetpack中的许多架构组件是专门为MVVM架构量身打造的,这一章主要介绍ViewModel、LiveData、Room。

简介

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

ViewModel

ViewModel的一个重要作用就是可以帮助Activity分担一部分工作,它是专门用于存放与界面相关的数据的。也就是说,只要是界面上能看得到的数据,它的相关变量都应该存放在ViewModel中,而不是Activity中。

ViewModel还有一个非常重要的特性,它可以保证在手机屏幕发生旋转的时候不会被重新创建,只有当Activity退出的时候才会跟着Activity一起销毁。

QQ_1737786896959

基本用法

通常来讲,比较好的编程规范是给每一个Activity和Fragment都创建一个对应的ViewModel。

需要添加依赖。

1
2
3
4
dependencies { 
...
implementation "androidx.lifecycle:lifecycle-extensions:2.2.0"
}

绝对不可以直接去创建ViewModel的实例,而是一定要通过ViewModelProvider来获取ViewModel的实例。

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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 = ViewModelProvider(this).get(MainViewModel::class.java)
plusOneBtn.setOnClickListener {
viewModel.counter++
refreshCounter()
}
refreshCounter()
}
private fun refreshCounter() {
infoText.text = viewModel.counter.toString()
}
}

传递构造参数

解决在构造函数中传递参数的问题,借助ViewModelProvider.Factory实现,必须实现create方法。

1
2
3
4
5
6
7
8
9
10
11
class MainViewModel(countReserved: Int) : ViewModel() { 
var counter = countReserved
}

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

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

}

Lifecycles

在一个非Activity的类中感知Activity生命周期。

隐式Fragment

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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()
}
}

Lifecycles组件

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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")
}
}
class MainActivity : AppCompatActivity() {
...
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
...
lifecycle.addObserver(MyObserver())
}
...
}

我们在方法上使用了@OnLifecycleEvent注解,并传入了一种生命周期事件。生命周期事件的类型一共有7种:ON_CREATE、ON_START、ON_RESUME、ON_PAUSE、ON_STOP和ON_DESTROY分别匹配Activity中相应的生命周期回调;另外还有一种ON_ANY类型,表示可以匹配Activity的任何生命周期回调。

LiveData

简单场景处理,同时将不会成为主流了,复杂场景需要学习flow、stateflow等替代。

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

LiveData内部不会判断即将设置的数据和原有数据是否相同,只要调用了setValue()或postValue()方法,就一定会触发数据变化事件。

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

另外,由于要减少性能消耗,当Activity处于不可见状态的时候(比如手机息屏,或者被其他的Activity遮挡),如果LiveData中的数据发生了变化,是不会通知给观察者的。只有当Activity重新恢复可见状态时,才会将数据通知给观察者。如果在Activity处于不可见状态的时候,LiveData发生了多次数据变化,当Activity恢复可见状态时,只有最新的那份数据才会通知给观察者,前面的数据在这种情况下相当于已经过期了,会被直接丢弃。

基本用法

注意Activity不能将实例传给ViewModel,否则会因为Activity无法释放而造成内存泄露。

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
33
34
35
36
37
class MainViewModel(countReserved: Int) : ViewModel() { 
val counter = MutableLiveData<Int>()
init {
counter.value = countReserved
}
fun plusOne() {
val count = counter.value ?: 0
// setValue()方法用于给LiveData设置数据,但是只能在主线程中调用;postValue()方法用于在非主线程中给LiveData设置数据。
counter.value = count + 1
}
fun clear() {
counter.value = 0
}
}

class MainActivity : AppCompatActivity() {
...
override fun onCreate(savedInstanceState: Bundle?) {
...
plusOneBtn.setOnClickListener {
viewModel.plusOne()
}
clearBtn.setOnClickListener {
viewModel.clear()
}
// 没有省略Observer,省略了就是JAVA的但抽象函数API
viewModel.counter.observe(this, Observer { count ->
infoText.text = count.toString()
})
}
override fun onPause() {
super.onPause()
sp.edit {
putInt("count_reserved", viewModel.counter.value ?: 0)
}
}
}

对于Observer不能省略的原因:这是一种非常特殊的情况,因为observe()方法接收的另一个参数LifecycleOwner也是一个单抽象方法接口。当一个Java方法同时接收两个单抽象方法接口参数时,要么同时使用函数式API的写法,要么都不使用函数式API的写法。由于我们第一个参数传的是this,因此第二个参数就无法使用函数式API的写法了。

不过在2019年的Google I/O大会上,Android团队官宣了Kotlin First,并且承诺未来会在Jetpack中提供更多专门面向Kotlin语言的API。lifecycle-livedata-ktx就是一个专门为Kotlin语言设计的库,这个库在2.2.0版本中加入了对observe()方法的语法扩展。现在就可以使用以下写法:

1
2
3
viewModel.counter.observe(this) { count -> 
infoText.text = count.toString()
}

推荐写法

由于上面的做法暴露了可变的LiveData给外部,推荐永远只暴露并不可变的LiveData给外部。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class MainViewModel(countReserved: Int) : ViewModel() { 
// 自定义getter,属于Kotlin中的属性访问器
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
}

fun clear() {
_counter.value = 0
}

}

map和switchMap

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

1
2
3
4
5
6
7
8
class MainViewModel(countReserved: Int) : ViewModel() { 

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

switchMap()方法,将这个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
26
27
28
29
30
31
32
data class User(var firstName: String, var lastName: String, var age: Int) 
object Repository {
fun getUser(userId: String): LiveData<User> {
val liveData = MutableLiveData<User>()
liveData.value = User(userId, userId, 0)
return liveData
}
}
class MainViewModel(countReserved: Int) : ViewModel() {
...
private val userIdLiveData = MutableLiveData<String>()
val user: LiveData<User> = Transformations.switchMap(userIdLiveData) { userId ->
Repository.getUser(userId)
}
fun getUser(userId: String) {
userIdLiveData.value = userId
}
}
class MainActivity : AppCompatActivity() {
...
override fun onCreate(savedInstanceState: Bundle?) {
...
getUserBtn.setOnClickListener {
val userId = (0..10000).random().toString()
viewModel.getUser(userId)
}
viewModel.user.observe(this, Observer { user ->
infoText.text = user.firstName
})
}
...

通过getUser(userId) 改变userIdLiveData -> 在switchMap观察到userIdLiveData改变后,执行lambda表达式中的内容 -> user.observe观察到user变化,回调执行lambda表达式中内容。

userIdLiveData是对外不可见的,是不能在外部观察的;user是可见的,是可以在外部观察的,从而将这个LiveData对象转换成另外一个可观察的LiveData对象。

Room

ORM也叫对象关系映射,将面向对象的语言和面向关系的数据库之间建立一种映射关系,这就是ORM了。

Room的整体结构。它主要由Entity、Dao和Database这3部分组成,详细说明如下。

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

由于Room会根据我们在项目中声明的注解来动态生成代码,因此这里一定要使用kapt引入Room的编译时注解库,而启用编译时注解功能则一定要先添加kotlin-kapt插件。注意,kapt只能在Kotlin项目中使用,如果是Java项目的话,使用annotationProcessor即可。

Entity

1
2
3
4
5
@Entity 
data class User(var firstName: String, var lastName: String, var age: Int) {
@PrimaryKey(autoGenerate = true)
var id: Long = 0
}

Dao

这部分是Room用法中最关键的地方。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@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

}

但是如果想要从数据库中查询数据,或者使用非实体类参数来增删改数据,那么就必须编写SQL语句了。Room是支持在编译时动态检查SQL语句语法的,如果编写的SQL语句有语法错误,编译的时候就会直接报错。

Database

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@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类必须继承自RoomDatabase类,并且一定要使用abstract关键字将它声明成抽象类。

databaseBuilder()方法接收3个参数,注意第一个参数一定要使用applicationContext,而不能使用普通的context,否则容易出现内存泄漏的情况。

另外,由于数据库操作属于耗时操作,Room默认是不允许在主线程中进行数据库操作的,为了方便测试,Room还提供了一个更加简单的方法,这个方法建议只在测试环境下使用,如下所示:

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

数据库升级

一般升级:

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

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

注意:Book表的建表语句必须和Book实体类中声明的结构完全一致,否则Room就会抛出异常。

数据库结构变化升级:

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
@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 author text not null
default 'unknown'")
}
}

private var instance: AppDatabase? = null

fun getDatabase(context: Context): AppDatabase {
...
return Room.databaseBuilder(context.applicationContext,
AppDatabase::class.java, "app_database")
.addMigrations(MIGRATION_1_2, MIGRATION_2_3)
.build().apply {
instance = this
}
}
}
}

比较有难度的就是编写SQL语句。

WorkManager

WorkManager和Service并不相同,也没有直接的联系。

  • Service是Android系统的四大组件之一,它在没有被销毁的情况下是一直保持在后台运行的。

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

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

基本用法

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

定义后台任务:

1
2
3
4
5
6
class SimpleWorker(context: Context, params: WorkerParameters) : Worker(context, params) { 
override fun doWork(): Result {
Log.d("SimpleWorker", "do work in SimpleWorker")
return Result.success()
}
}

doWork()方法不会运行在主线程当中。

配置该后台任 务的运行条件和约束信息:

这一步最为复杂。

1
2
3
val request = OneTimeWorkRequest.Builder(SimpleWorker::class.java).build() 
val request = PeriodicWorkRequest.Builder(SimpleWorker::class.java, 15,
TimeUnit.MINUTES).build() // 运行周期间隔不能短于15分钟

调用enqueue()方法执行:

1
WorkManager.getInstance(context).enqueue(request) 

处理复杂任务

WorkManager拥有以下功能:

  1. 让后台任务在指定的延迟时间后运行;
  2. 比如说给后台任务请求添加标签,可以通过标签来取消后台任务请求;
  3. doWork()方法中返回了Result.retry(),那么是可以结合setBackoffCriteria()方法来重新执行任务的;
  4. 对运行结果进行监听;
  5. 链式任务。

setBackoffCriteria()时间设置不能小于10s,同时可以设置线性延迟或者指数的方式延迟。

这是因为绝大多数的国产手机厂商在进行Android系统定制的时候会增加一个一键关闭的功能,允许用户一键杀死所有非白名单的应用程序。而被杀死的应用程序既无法接收广播,也无法运行WorkManager的后台任务。因此,这里给你的建议就是,WorkManager可以用,但是千万别依赖它去实现什么核心功能,因为它在国产手机上可能会非常不稳定


Kotlin课堂(了解)

DSL的全称是领域特定语言(Domain Specific Language),它是编程语言赋予开发者的一种特殊能力,通过它我们可以编写出一些看似脱离其原始语法结构的代码,从而构建出一种专有的语法结构。

比如Gradle是一种基于Groovy语言的构建工具,因此如下的语法结构其实就是Groovy提供的DSL功能,build.gradle文件如下:

1
2
3
4
dependencies { 
implementation 'com.squareup.retrofit2:retrofit:2.6.1'
implementation 'com.squareup.retrofit2:converter-gson:2.6.1'
}

我们实现Kotlin版本的DSL功能,代码如下:

1
2
3
4
5
6
7
8
9
10
11
class Dependency { 
val libraries = ArrayList<String>()
fun implementation(lib: String) {
libraries.add(lib)
}
}
fun dependencies(block: Dependency.() -> Unit): List<String> {
val dependency = Dependency()
dependency.block()
return dependency.libraries
}

好了,现在可以使用以下的语法,build.gradle文件如下:

1
2
3
4
dependencies { 
implementation("com.squareup.retrofit2:retrofit:2.6.1")
implementation("com.squareup.retrofit2:converter-gson:2.6.1")
}

这就是Kotlin版本的Gradle构建文件。