Jetpack

Jetpack是一个开发组件工具集,它的主要目的是帮助我们编写出更加简洁的代码,并简化我们的开发过程。Jetpack中的组件有一个特点,它们大部分不依赖于任何Android系统版本,这意味着这些组件通常是定义在AndroidX库当中的,并且拥有非常好的向下兼容性。

MVVM架构

ViewModel

ViewModel是可以帮助Activity分担一部分工作,它是专门用于存放与界面相关的数据的。
只要是界面上能看得到的数据,它的相关变量都应该存放在ViewModel中,而不是Activity中,这样可以在一定程度上减少Activity中的逻辑。
ViewModel还有一个非常重要的特性。我们都知道,当手机发生横竖屏旋转的时候,Activity会被重新创建,同时存放在Activity中的数据也会丢失。而ViewModel的生命周期和Activity不同,它可以保证在手机屏幕发生旋转的时候不会被重新创建,只有当Activity退出的时候才会跟着Activity一起销毁。

导入外部依赖implementation "androidx.lifecycle:lifecycle-extensions:2.2.0"

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class MainViewModel : ViewModel() {
var counter = 0
}
...
//Activity中
viewModel = ViewModelProvider(this)[MainViewModel::class.java]
binding.apply {
plusOneBtn.setOnClickListener {
viewModel.counter++
refreshCounter()
}
}
}
private fun refreshCounter() {
binding.infoText.text = viewModel.counter.toString()
}

这一段的一个重点:我们不是创建一个viewModel实例,而是通过ViewModelProvider来获取ViewModel的实例.因为ViewModel有其独立的生命周期,并且其生命周期要长于Activity。

向viewModel传递参数

由于所有ViewModel的实例都是通过ViewModelProvider来获取的,因此我们没有任何地方可以向ViewModel的构造函数中传递参数。只能借助ViewModelProvider.Factory来实现

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

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

这样子,ViewModel就可以接收一个参数了,然后通过factory去构建他。
为什么这里可以创建了呢?因为create()方法的执行时机和Activity的生命周期无关,所以不会产生之前提到的问题:当activity被销毁后,viewmodel也一起销毁了。
使用方法是:

1
2
3
4
viewModel = ViewModelProvider(
this,
MainViewModelFactory(countReserved)
)[MainViewModel::class.java]

只有通过这种方法创建才能传入参数,并且保证viewModel的声明周期与activity的声明周期相互独立。(这里你也可以使用object继承,而不用单独创建一个工厂类。

Lifecycles

我们需要能够时刻感知到Activity的生命周期,以便在适当的时候进行相应的逻辑控制。
可是而如果要在一个非Activity的类中去感知Activity的生命周期,应该怎么办呢?

LifecycleObserver是一个空方法接口,如果想要感知到Activity的生命周期,还得借助额外的注解功能才行.

1
2
3
4
5
6
7
8
9
10
11
12
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")
}
}

然后再Activity的创建中lifecycle.addObserver(MyObserver())将我们构建的类添加进去,他就会接收到生命周期的信息,每当生命周期发生变化,他就会执行方法。
但是这样子也只会被动的接收生命周期的改变。无法主动感知生命周期的变化

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

只有把lifecycle对象传递进去,就可以再任意地方调用lifecycle.currentState来主动获取当前的生命周期状态。
lifecycle.currentState返回的生命周期状态是一个枚举类型,一共有INITIALIZED、DESTROYED、CREATED、STARTED、RESUMED这5种状态类型

LiveData

它可以包含任何类型的数据,并在数据发生变化的时候通知给观察者。他通常与viewModel一起使用
问题:如果ViewModel的内部开启了线程去执行一些耗时逻辑,那么在点击按钮后就立即去获取最新的数据,得到的肯定还是之前的数据。也就是意味着,只有activity能够主动向viewModel索要数据,而不能通过viewModel主动向activity提供数据。而如果我们再veiwModel中存储activity的实例,又会导致生命周期过长,activity的内存占用无法释放。
因此可以将数据交由LiveData包装,然后在activity中观察该数它。就可以主动的将变化通知给Activity了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class MainViewModel(val countReserved: Int) : ViewModel() {
var counter = MutableLiveData<Int>()

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

将数据通过MutableLiveData包装起来,它提供三个方法
分别是getValue()、setValue()和postValue()方法。
getValue()方法用于获取LiveData中包含的数据;
setValue()方法用于给LiveData设置数据,但是只能在主线程中调用;
postValue()方法用于在非主线程中给LiveData设置数据。
然后在Activity中修改按钮的绑定事件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
 binding.apply {
plusOneBtn.setOnClickListener {
viewModel.plusOne()

}
clearBtn.setOnClickListener {
viewModel.clear()
}
//观察者,当检测到数据变化时,就会调用后面的方法。lambda函数中的参数就是value值
viewModel.counter.observe(this@MainActivity, Observer { count ->
infoText.text = count.toString()
})
}
}
//当反转屏幕时,或者退出应用时。activity,将其存储起来
override fun onPause() {
super.onPause()
sp.edit().apply {
putInt("count_reserved", viewModel.counter.value ?: 0)
}

observe()方法接收两个参数:第一个参数是一个LifecycleOwner对象,,Activity本身就是一个LifecycleOwner对象,因此直接传this就好;第二个参数是一个Observer接口,当counter中包含的数据发生变化时,就会回调到这里,因此我们在这里将最新的计数更新到界面上即可。
提问:为什么这里不使用函数式API的写法呢?
因为observe()方法接收的另一个参数LifecycleOwner也是一个单抽象方法接口。当一个Java方法同时接收两个单抽象方法接口参数时,要么同时使用函数式API的写法,要么都不使用函数式API的写法。由于我们第一个参数传的是this,因此第二个参
数就无法使用函数式API的写法了。

比较推荐的做法是,永远只暴露不可变的LiveData给外部。这样在非ViewModel中就只能观察LiveData的数据变化,而不能给LiveData设置数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class MainViewModel(val countReserved: Int) : ViewModel() {
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
}
}

这是官方最推荐的写法。它无法修改counter的值

map和switchMap

map

作用:将实际包含数据的LiveData和仅用于观察数据的LiveData进行转化。
使用场景:当我们一个User对象中有姓名,年龄,地址等属性。而用户只需要知道姓名即可。年龄和地址都是不需要暴露的。此时如果暴露这些信息就不合适了。所以使用map可以将数据转化,如下:

1
2
3
4
5
6
class MainViewModel(val countReserved: Int) : ViewModel() {
private val userLiveData = MutableLiveData<User>()
val userName:LiveData<String> = Transformations.map(userLiveData){
it.firstName+it.lastName
}
}

当userLiveData的数据发生变化时,map()方法会监听到变化并执行转换函数中的逻辑,然后再将转换之后的数据通知给userName的观察者。从以前只有一个observer观察,到现在多了一个map也会观察数据的变化

swichMap

他的使用场景只有一个,但也极为重要:如果你获取的LivaData对象不是从那唯一的对象中获取,而是每次都会新建一个返回的话。就会出现一个问题,”observere只会监测最开始出现的那个对象,之后的对象就即便出现也不会检测到”。所以需要使用swichMap,例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
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
}
fun refresh() {
refreshLiveData.value = refreshLiveData.value
}
}

这里即便是调用刷新,refresh方法,都会被swichMap检测到,然后他会通知前台的observer这个数据已经被更改了。而且她也只会把最新的数据通知出去。之前的数据会被丢弃

Room

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

1
2
3
4
5
6
apply plugin: 'kotlin-kapt'
dependencies {
...
implementation "androidx.room:room-runtime:2.1.0"
kapt "androidx.room:room-compiler:2.1.0"
}

其中kotlin-kapt插件是用来启用注解功能的。
具体框架

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
38
39
40
41
42
43
44
45
46
47
48
49
50

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

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

@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关键字将它声明成抽象类,然后提供相应的抽象方法
原则上全局应该只存在一份AppDatabase的实例。这里使用了instance变量来缓存AppDatabase的实例,然后在getDatabase()方法中判断:如果instance变量不为空就直接返回,否则就调用Room.databaseBuilder()方法来构建一个AppDatabase的实例。databaseBuilder()方法接收3个参数,注意第一个参数一定要使用applicationContext,而不能使用普通的context,否则容易出现内存泄漏的情况
第二个参数是AppDatabase的Class类型,第三个参数是数据库名,这些都比较简单。最后调用build()方法完成构建,并将创建出来的实例赋值给instance变量,然后返回当前实例即可。

由于数据库操作属于耗时操作,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
@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
}
}
}
}

在companion object结构体中,我们实现了一个Migration的匿名类,并传入了1和 2这两个参数,表示当数据库版本从1升级到2的时候就执行这个匿名类中的升级逻辑。匿名类实例的变量命名也比较有讲究,这里命名成MIGRATION_1_2,可读性更
高。由于我们要新增一张Book表,所以需要在migrate()方法中编写相应的建表语句。另外必须注意的是,Book表的建表语句必须和Book实体类中声明的结构完全一致,否则Room就会抛出异常。
有时候,我们的升级不一定需要新建一张表,而只要新增一列,所以查看实例:

1
2
3
4
5
6
7
8
9
···
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'")
}
}
···
.addMigrations(MIGRATION_1_2, MIGRATION_2_3)

WorkManager

从4.4系统开始AlarmManager的触发时间由原来的精准变为不精准,5.0系统中加入了JobScheduler来处理后台任务,6.0系统中引入了Doze和App Standby模式用于降低手机被后台唤醒的频率,从8.0系统开始直接禁用了Service的后台功能,只允许使用前台Service。
这么频繁的功能和API变更,让开发者就很难受了,到底该如何编写后台代码才能保证应用程序在不同系统版本上的兼容性呢?
WorkManager可以根据操作系统的版本自动选择底层是使用AlarmManager实现还是JobScheduler实现,从而降低了我们的使用成本。另外,它还支持周期性任务、链式任务处理等功能,是一个非常强大的工具。它可以保证即使在应用退出甚至手机重启的情况下,之前注册的任务仍然将会得到执行,因此WorkManager很适合用于执行一些定期和服务器进行交互的任务,比如周期性地同步数据,等等。

基本用法

先导入依赖

1
implementation "androidx.work:work-runtime:2.2.0"

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

  1. 定义一个后台任务,并实现具体的任务逻辑;

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

    doWork()方法不会运行在主线程当中,可以用来执行耗时任务。
    成功就返回Result.success(),失败就返回Result.failure()

  2. 配置该后台任务的运行条件和约束信息,并构建后台任务请求;
    val request = OneTimeWorkRequest.Builder(SimpleWorker::class.java).build()
    val request = PeriodicWorkRequest.Builder(SimpleWorker::class.java, 15, TimeUnit.MINUTES).build()后一个代码端相比于前代码,多了个周期,会每15分钟执行一次。

  3. 将该后台任务请求传入WorkManager的enqueue()方法中,系统会在合适的时间运行。
    最后一步WorkManager.getInstance(context).enqueue(request)

处理复杂任务

设置延时任务:

1
2
3
val request = OneTimeWorkRequest.Builder(SimpleWorker::class.java)
.setInitialDelay(5, TimeUnit.MINUTES)
.build()

添加标签:

1
2
3
4
val request = OneTimeWorkRequest.Builder(SimpleWorker::class.java)
...
.addTag("simple")
.build()

添加标签后,可以通过标签关掉该任务
WorkManager.getInstance(this).cancelAllWorkByTag("simple")
当然,即使没有标签,也可以通过id来取消后台任务请求:
WorkManager.getInstance(this).cancelWorkById(request.id)
使用id只能取消单个后台任务请求,而使用标签的话,则可以将同一标签名的所有后台任务请求全部取消,这个功能在逻辑复杂的场景下尤其有用。

设置任务重新执行:如果后台任务的doWork()方法中返回了Result.retry()那么是可以结合setBackoffCriteria()方法来重新执行任务

1
2
3
4
val request = OneTimeWorkRequest.Builder(SimpleWorker::class.java)
...
.setBackoffCriteria(BackoffPolicy.LINEAR, 10, TimeUnit.SECONDS)
.build()

Result.success()和Result.failure()又有什么作用?这两个返回值其实就是用于通知任务运行结果的

1
2
3
4
5
6
7
8
9
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")
}
}

链式任务:

1
2
3
4
5
6
7
8
 val sync = ...
val compress = ...
val upload = ...
WorkManager.getInstance(this)
.beginWith(sync)
.then(compress)
.then(upload)
.enqueue()

也就是说,如果某
个后台任务运行失败,或者被取消了,那么接下来的后台任务就都得不到运行了