长久以来,Android 官方并没有指定一个项目架构的规范,只要能够实现功能,代码怎么编写都是你的自由。但是不同人的技术水平不同,最终编写出来的代码是千差万别的。 为了追求更高的代码质量,慢慢的就有第三方的社区和开发者将一些更加高级的项目架构引入到了 Android 平台上,如 MVP、MVVM 等。使用这些架构开发出来的应用程序在代码质量、可读性、易维护性等方面都有着出色的表现,于是这些框架逐渐成为了主流。 后来 Google 或许意识到了这个情况,终于在 2017 年,推出了一个官方的架构组件库——Architecture Components,意在帮助开发者编写出更加符合高质量代码规范、更具有架构设计的应用程序。 2018 年 Google 又推出了一个全新的开发组件工具集——Jetpack,并将 Architecture Components 作为 Jetpack 的一部分纳入其中。 2019 年又有许多新的组件被加入 Jetpack 中,未来的 Jetpack 还会不断地继续扩充。
一、Jetpack简介
Jetpack 是一个开发工具集,它的主要目的是帮助我们编写出更加简洁的代码,并简化我们的开发过程。Jetpack 中的组件有一个特点,它们大部分不依赖于任何 Android 系统版本,这意味着这些组件通常是定义在 AndroidX 库当中的,并且拥有非常好的向下兼容性。下图是一张 Jetpack 的“全家福”:
可以看到,Jetpack 的家族还是非常庞大的,主要由基础、架构、行为、界面这 4 个部分组成。Jetpack 并不全是些新东西,只要是能够帮助开发者更好更方便的构建应用程序的组件,Google 都将其纳入了 Jetpack。
目前 Android 官方最为推荐的项目架构就是 MVVM,因而 Jetpack 中的许多架构组件是专门为 MVVM 架构量身打造的。
二、ViewModel
ViewModel 可以说是 Jetpack 中最重要的组件之一了,而 ViewModel 的一个重要的作用就是可以帮助 Activity 分担一部分工作,它是专门用于存放与界面相关的数据的。也就是说,只要是界面上能看到的数据,它的相关变量都应该存放在 ViewModel 中,而不是 Activity 中,这样可以在一定程度上减少 Activity 中的逻辑。
另外,ViewModel 还有一个非常重要的特性。当手机发生竖屏旋转的时候,Activity 会被重新创建,同时存放在 Activity 中的数据也会丢失。而 ViewModel 的生命周期和 Activity 不同,它可以保证在手机屏幕发生旋转的时候不会被重新创建,只有当 Activity 退出的时候才会跟着 Activity 一起销毁,因此,将与界面相关的变量存放在 ViewModel 当中,这样即使旋转手机屏幕,界面上显示的数据也不会丢失。ViewModel 的生命周期如图所示:
1.ViewModel的基本用法
由于 Jetpack 中的组件通常是以 AndroidX 库的形式发布的,因此一些常用的 Jetpack 组件会在创建Android 项目的时候自动被包含进去。不过如果我们想要使用 ViewModel 组件,还需要在 `app/build.gradle 文件中添加如下依赖:
dependencies {
implementation "androidx.lifecycle:lifecycle-extensions:2.1.0"
}
通常来讲,比较好的编程规范是给每一个 Activity 和 Fragment 都创建一个对应的ViewModel类,并让它继承自ViewModel,代码如下所示:
class MainViewModel : ViewModel() {}
根据前面所学的知识,所有与界面有关的数据都应该放在 ViewModel 中,那么这里如果我们要实现一个计数器的功能,就可以在 ViewModel 中加入一个 counter
变量用于计数,如下所示:
class MainViewModel : ViewModel() {
var counter = 0
}
下面实现与 ViewModel 对应的 MainActivity 中的代码:
class MainActivity : AppCompatActivity() {
lateinit var viewModel: MainViewModel
override fun onCreate(savedInstanceState: Bundle?) {
...
// 获取ViewModel实例,不能直接创建实例
// 而是要通过ViewModelProvider来获取
viewModel = ViewModelProvider(this)
.get(MainViewModel::class.java)
// 按钮事件是给counter加1,并打印加1后的数据
btn.setOnClickListener {
viewModel.counter++
println(viewModel.counter.toString())
}
}
}
上面之所以不可以直接去创建 ViewModel 实例,是因为 ViewModel 有独立的生命周期,并且生命周期要长于 Activity,如果我们在 onCreate()
方法中创建 ViewModel 实例,那么每次 onCreate() 方法执行的时候,ViewModel 都会创建一个新的实例,这样当手机屏幕发生旋转的时候,就无法保留其中的数据了。
2.向 ViewModel 传递参数
上一小节的构造函数中没有任何参数,如果我们确实需要通过构造函数来传递一些参数,应该怎么办?由于所有的 ViewModel 实例都是通过 ViewModel 参数来获取的,因此我们没有任何地方可以向 ViewModel 的构造函数中传递参数。
当然这个问题也不难解决,只需要借助 ViewModelProvider.Factory
就可以实现了。下面通过具体的例子来学习一下。
修改上一小节的 MainViewModel 中的代码:
// 给构造函数添加一个参数,用于记录之前保存的值
// 并在初始化的时候赋值给了counter
class MainViewModel(countReservesd: Int): ViewModel() {
var counnter = countReserved
}
实现 ViewModelProvider.Factory 接口。新建 MainViewModelFactory 类:
// 接收countReserved参数
class MainViewModelFactory(private val countReserved: Int) :
ViewModelProvider.Factory {
// 必须实现create()方法
override fun <T : ViewModel> create(modelClass: Class<T>): T {
// 创建MainViewModel实例并返回
// create()方法的执行时机和Activity生命周期无关
// 所以这里直接创建实例不会产生之前提到的问题
return MainViewModel(countReserved) as T
}
}
修改 MainActivity 中获取 MainViewModel 实例代码:
class MainActivity : AppCompatActivity() {
lateinit var viewModel: MainViewModel
override fun onCreate(...) {
...
// 在ViewModelProvider构造函数中额外传入Factory的实例
viewModel = ViewModelProvider(this,
MainViewModelFactory(countReserved))
.get(MainViewModel::class.java)
...
}
}
三、Lifecycles
在编写 Android 应用程序的时候,可能会经常遇到需要感知 Activity 生命周期的情况。比如说某个界面发起了一条网络请求,但是当请求得到相应的时候,界面或许已经关闭了,这个时候就不应该继续对响应的结果进行处理。因此我们需要能够时刻感知到 Activity 的生命周期以便在适当的时侯进行相应的逻辑控制。
感知 Activity 的生命周期并不复杂,但问题在于在一个非 Activity 的类中去感知 Activity 的生命周期应该怎么办呢?
Lifecycles 组件就是为了解决这个问题而出现的,它可以让任何一个类都能轻松感知到 Activity 的生命周期,同时又不需要在 Activity 中编写大量的逻辑处理。
1.Lifecycles的基本用法
下面通过具体的例子来学习 Lifecycles 组件的用法。新建 MyObserver 类,并实现 LifecycleObserver 接口:
// MyObserver中可以定义任何方法
// 但如果想要感知Activity生命周期就需要额外的注解才行
class MyObserver : LifecyclesObserver {
// 在Activity的onStart()时触发
@OnLifecycleEvent(Lifecycle.Event.ON_START)
fun activityStart() {
Log.d("MyObserver", "activityStart")
}
// 在Activity的onStop()时触发
@onLifecycleEvent(Lifecycle.Event.ON_STOP)
fun activityStop() {
Log.d("MyObserver", "activityStop")
}
}
@OnLifecycleEvent
注解生命周期事件的类型一共有7种:ON_CREATE
、ON_START
、ON_RESUME
、ON_PAUSE
、ON_STOP
、ON_DESTROY
和 ON_ANY
,其中 ON_ANY 表示匹配 Activity 的任何生命周期回调,其他的分别对应着 Activity 中相应的生命周期回调。
下面代码可以将 MyObserver 添加到 Activity 中:
lifecycleOwner.lifecycle.addObserver(MyObserver())
首先调用 LifecycleOwner 的 getLifecycle()
方法得到一个 Lifecycle 对象,然后调用它的 addObserver()
方法来观察 LifecycleOwner 的生命周期,再把 MyObserver 的实例传进去就可以了。
如果你的 Activity 继承自 AppCompatActivity 或者 Fragment 继承自 androidx.fragment.app.Fragment,那么它本身就是一个 LifecycleOwner 实例,这部分工作已经由 AndroidX 库自动帮我们完成了,也就是说在 MainActivity 中当中就可以直接这样写:
class MainActivity : AppCompatActivity() {
override fun onCreate(...) {
...
// 添加这行代码后MyObserver就可以感知到Activity生命周期
lifecycle.addObserver(MyObserver())
}
}
上面的方法同样也适用于 Fragment。
2.主动获知当前的生命周期状态
// 将Lifecycle对象传进来即可
// 调用lifecycle.currentState来主动获知当前的生命周期状态
class MyObserver(val lifecycle: Lifecycle) : LifecycleObserver {
}
lifecycle.currentState 返回的生命周期是一个枚举类型,一共有 INITIALIZED
、DESTROYED
、CREATED
、STARTED
、RESUMED
这五种状态类型。它们与 Activity 的生命周期回调所对应的关系如图所示:
也就是说当获取的生命周期状态是 CREATED 的时候,说明 onCreate() 方法已经执行了,但是 onStart() 方法还没有执行。 当获取的生命周期是 STARTED 的时候,说明 onStart() 方法已经执行了, 但是 onResume() 方法还没有执行,以此类推。
四、LiveData
LiveData 是 Jetpack 提供的一种响应式编程组件,它可以包含任何类型的数据,并在数据发生变化的时候通知给观察者。LiveData 特别适合与 ViewModel 结合在一起使用,虽然它也可以单独用在别的地方,但是在绝大多数情况下,它是使用在 ViewModel 当中的。
1.LiveData的基本用法
根据上一小章的 ViewModel 进行修改,在 Activity 中对 counter 进行观察,在 counter 变量的数据发生变化之后可以主动通知 Activity。
修改MainViewModel中的代码:
class MainViewModel(countReserved: Int) : ViewModel() {
// 将counter变量改成MutableLiveData类型
// 指定泛型为Int,代表它包含的是整形数据
// MutableLiveData是一种可变的LiveData,主要有三种读写数据方法
// getValue():获取LiveData中包含的数据
// setValue():给LiveData设置数据,只能在主线程调用
// postValue():在非主线程中给LiveData设置数据
val counter = MutableLiveData<Int>()
init {
counter.value = countReserved
}
// 给counter计数器加1
fun plusOne() {
// counter的getValue()语法糖写法
val count = counter.value ?: 0
// counter的setValue()语法糖写法
counter.value = count + 1
}
// 给counter计数器清零
fun clear() {
// counter的setValue()语法糖写法
counter.value = 0
}
}
下面修改 MainActivity 代码:
class MainActivity : AppCompatActivity() {
...
override fun onCreate(...) {
...
plusOneBtn.setOnClickListener {
viewModel.plusOne()
}
clearBtn.setOnClickListener {
viewModel.clear()
}
// 调用counter的observer方法来观察数据的变化
// 第一个参数接收LifecycleOwner对象
// 第二个参数是一个Observer接口
// 当counter中数据发生变化时就会回调到这里
viewModel.counter.observe(this) { count ->
println(count.toString())
}
}
}
想要对 observer 使用 Kotlin 语法特性的话可能需要引入如下依赖:
dependencies {
implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.2.0"
}
2.LiveData 更推荐的做法
上一小节的写法虽然可以正常工作,但其实任然不是最规范的 LiveData 用法,主要问题就在于我们将 counter 这个可变的 LiveData 暴露给了外部。这样即使是在 ViewModel 的外面也是可以给 counter 设置数据的,从而破坏了 ViewModel 数据的封装性,同时也可能带来一定的风险。
比较推荐的做法是,永远只暴露不可变的 LiveData 给外部,这样在非 ViewModel 中就只能观察 LiveData 的数据变化,而不能给 LiveData 设置数据。下面改造 MainViewModel:
class MainViewModel(countReserved: Int) : ViewModel() {
// 对外的counter设置为不可变LiveData
// 变量的get()方法返回_counter数据
val counter: LiveData<Int>
get() = _counter
// 给可变LiveData加上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
}
}
3.map 和 switchMap
LiveData 的基本用法虽说可以满足大部分的开发需求,但是当项目变得复杂了之后,可能会出现一些更加特殊的需求。LiveData 为了能够应对各种不同的需求场景,提供了两种转换方法:map()
和 switchMap()
方法。
3.1.map()
map()
方法的作用是将实际包含数据的 LiveData 和仅用于观察数据的 LiveData 进行转换。下面举例一个使用到这个方法的地方。
比如说有一个 User 类,User 中包含用户的姓名和年龄,定义如下:
data class User(var firstName: String, var lastName: String,
var age: Int)
我们可以在 ViewModel 中创建一个相应的 LiveData 来包含 User 类型的数据,如下所示:
class MainViewModel(countReserved: Int) : ViewModel() {
val userLiveData = MutableLiveData<User>()
}
如果 MainActivity 中明确只会显示用户的姓名,而完全不关心用户的年龄,那么这个时候还将整个 User 类型的 LiveData 暴露给外部,就显得不那么合适了。
map() 方法就是专门用于解决这种问题的,它可以将 User 类型的 LiveData 自由地转型成任意其他类型的 LiveData,下面看一下具体用法:
class MainViewModel(countReserved: Int) : ViewModel() {
// 添加private保证数据的封装性
private val userLiveData = MutableLiveData<User>()
// 调用map()方法来对LiveData的数据类型进行转换
// 第一个参数是原始的LiveData对象
// 第二个参数是一个转换函数,在其中编写具体的转换逻辑即可
// 这里的逻辑就是把User对象转换成只包含用户姓名的字符串
// 外部只需观察userName即可
val userName: LiveData<String> = Transformations.map(userLiveData) { user ->
"${user.firstName} ${user.lastName}"
}
}
3.2.switchMap()
switchMap()
的使用场景非常固定,但是可能比 map() 方法要更加常用。
前面所学的内容都有一个前提:LiveData 对象的实例都是在 ViewModel 中创建的。然而在实际的项目中,不可能一直是这种理想情况,很可能 ViewModel 中的某个 LiveData 对象是调用另外的方法获取的。
下面就来模拟一下这种情况,新建一个 Repository 单例类,代码如下所示:
object Repository {
// 返回包含数据的 LiveData 对象
fun getUser(userId: String): LiveData<User> {
val liveData = MutableLiveData<User>()
liveData.value = User(userId, userId, 0)
return liveData
}
}
然后在 ViewModel 中也定义一个 getUser()
方法,并且让它调用 Repository 的 getUser() 方法来获取 LiveData 对象:
class MainViewModel(...) : ViewModel() {
...
// 在Activity中不能直接使用getUser(userId).observe来观察
// 因为上述写法会一直观察老的LiveData实例
// 这个时候switchMap()方法就可以派上用场了
fun getUser(userId: String): LiveData<User> {
return Repository.getUser(userId)
}
}
正如前面所说,switchMap() 的使用场景非常固定,如果 ViewModel 中的某个 LiveData 对象是调用的另外的方法或取的,那么我们就可以借助 switchMap() 方法,将这个 LiveData 对象转换成另一个可观察的 LiveData 对象。修改 MainViewModel 代码:
class MainViewModel(...) : ViewModel() {
...
// 用来观察userId的数据变化
private val userIdLiveData = MutableLiveData<String>()
// 对另一个可观察的LiveData对象进行转换
val user: LiveData<User> =
Transformations.switchMap(userIdLiveData) { userId ->
// 注意!!!
// 必须在这个转换函数中返回一个LiveData对象
// 因为switchMap()工作原理就是将转换函数中返回的LiveData-
// -转换成另一个可观察的LiveData对象,那么很显然
// 我们只需调用Repository.getUser()得到的LiveData直接返回即可
Repository.getUser(userId)
}
fun getUser(userId: String) {
userIdLiveData.value = userId
}
}
为了更清晰的理解 switchMap() 的用法,再来梳理一遍它的整体工作流程。首先,当外部调用 MainViewModel 的 getUser() 方法来获取用户数据时,并不会发起任何请求或者函数调用,只会将传入的 userId 值设置到 userIdLiveData 当中。一旦 userIdLiveData 的数据发生变化,那么观察 userIdLiveData 的 switchMap() 方法就会执行,并且调用我们编写的转换函数。然后在转换函数中调用 Repository.getUser() 方法获取真正的用户数据。同时,switchMap() 方法会将 Repository.getUser() 方法返回的 LiveData 对象转换成一个可观察的 LiveData 对象,对于 Activity 而言,只要去观察这个 LiveData 对象就可以了。
五、Room
之前学习过 SQLite 数据库的使用方法,不过当时仅仅是使用了一些原生的 API 来进行数据的增删改查操作。这些原生 API 虽然简单易用,但是如果放到大型项目当中的话,会非常容易让项目的的代码变得混乱。
为此市面上出现了诸多专门为 Android 数据库设计的 ORM 框架,ORM(Object Relational Mapping)也叫对象关系映射。简单来讲,我们使用的编程语言是面向对象语言,而使用的数据库则是关系型数据库,将面向对象的语言和面向关系的数据库之间建立一种映射关系,这就是 ORM 了。
ORM 的好处就是可以直接用面向对象的思维来和数据库进行交互,绝大多数情况下不用再和 SQL 语句打交道了,同时也不用担心操作数据库的逻辑会让项目的整体代码变得混乱。
1.使用 Room 前准备
Room 整体结构主要由 Entity、Dao 和 Database 这三部分组成,每个部分都有明确的职责,详细说明如下:
- Entity 用于定义封装实际数据的实体类,每个实体类都会在数据库中有一张对应的表,并且表的列是根据实体类中的字段自动生成的。
- Dao 数据库访问对象,通常会在这里对数据库的各项操作进行封装,在实际编程的时候,逻辑层就不需要和底层数据库打交道了,直接和Dao层进行交互即可。
- Database 用于定义数据库中的关键信息,包括数据库的版本号、包含哪些实体类以及提供Dao层的访问实例。 使用Room前,还需要在app/build.gradle文件中添加如下的依赖:
plugins {
...
// 由于Room会根据项目中声明的注解来动态生成代码
// 所以这里一定要使用kapt引入Room的编译时注解库
// 而启用编译时注解功能一定要先添加kotlin-kapt插件
// Java项目中使用 annotationProcessor
id 'kotlin-kapt'
}
dependencies {
implementation "androidx.room:room-runtime:2.2.5"
kapt "androidx.room:room-compiler:2.2.5"
}
2.Room 基本使用
1.定义 Entity
// @Entity注解声明Entity类,对应一张表
@Entity
data class User(var firstName: String, var lastName: String,
var age: Int) {
// 给每一个实体类都添加一个id字段
// 注解将id字段声明成主键,并使主键值自动生成(增加)
@PrimaryKey(autoGenerate = true)
var id: Long = 0
}
2.定义 Dao
Dao 是 Room 用法中最关键的地方,所有访问数据库的操作都是在这里封装的。访问数据库的操作无非就是增删改查这 4 种,但是业务需求却是千变万化的。而 Dao 要做的事情就是覆盖所有的业务需求,使得业务方永远只需要与 Dao 层进行交互,而不必和底层的数据库打交道。
// 使用@Dao注解
@Dao
interface UserDao {
// @Insert表示将参数中传入的User插入数据库,并返回主键id
@Insert
fun insertUser(user: User): Long
// @Update表示将参数中传入的User对象更新到数据库中
@Update
fun updateUser(newUser: User)
// 如果想要查询数据,或者使用非实体类参数来增删改查数据——
// ——那么就必须编写SQL语句了
@Query("SELECT * FROM User")
fun loadAllUsers(): List<User>
@Query("SELECT * FROM User WHERE age > :age")
fun loadUsersOlderThan(age: Int): List<User>
// @Delete表示会将传入的User对象从数据库中删除
@Delete
fun deleteUser(user: User)
@Query("DELETE FROM User WHERE lastName = :lastName")
fun deleteUserByLastName(lastName: String): Int
}
3.定义 Database
这部分的写法是非常固定的,只需要定义好 3 个部分的内容:数据库的版本号、包含哪些实体类、以及提供 Dao 层的访问实例。创建一个 AppDatabase.kt:
// 在注解中声明数据库版本以及包含哪些实体类,多个实体类逗号隔开
// 一定要使用抽象类,并提供获取Dao层的抽象方法
@Database(version = 1, entities = [User::class])
abstract class AppDatabase : RoomDatabase() {
// 只用声明方法,具体实现由Room在底层自动完成
abstract fun userDao(): UserDao
companion object {
// 单例模式
// 原则上全局只应存在一份AppDatabase实例
private var instance: AppDatabase? = null
@Synchronized
fun getDatabase(context: Context): AppDatabase {
instance?.let {
return it
}
// 构建AppDatabase实例并返回
// 第一个参数一定要使用applicationContext
// 不能直接使用context,否则容易出现内存泄漏
// 第三个参数是数据库名
return Room.databaseBuilder(
context.applicationContext,
AppDatabase::class.java,
"app_database").build().apply {
instance = this
}
}
}
}
4.实现增删改查逻辑
class MainActivity : AppCompatActivity() {
override fun onCreate(...) {
...
val userDao = AppDatabase.getDatabase(this).userDao()
val user1 = User("Tom", "Brady", 40)
val user2 = User("Tom", "Hanks", 63)
addDataBtn.setOnClickListener {
thread {
// 后面删除和更新操作基于这个ID来操作
user1.id = userDao.insertUser(user1)
user2.id = userDao.insertUser(user2)
}
}
updateDataBtn.setOnClickListener {
thread {
user1.age = 42
userDao.updateUser(user1)
}
}
deleteDataBtn.setOnClickListener {
thread {
userDao.deleteUserByLastName("Hanks")
}
}
queryDataBtn.setOnClickListener {
thread {
for (user in userDao.loadAllUsers()) {
Log.d("MainActivity", user.toString())
}
}
}
}
}
5.在主线程中操作数据库
由于数据库操作属于耗时操作,Room 默认是不允许在主线程中进行数据库操作的。不过为了方便测试,Room 还提供了一个更加简单的方法,如下所示:
Room.databaseBuilder(context.applicationContext,
AppDatabase::class.java, "app_database")
// 只建议在测试环境下使用
.allowMainThreadQueries()
.build()
3.Room数据库升级
1.开发阶段暴力升级
如果目前只是在开发测试阶段,不想编写那么麻烦的数据库升级逻辑,Room 倒也提供了一个简单粗暴的方法,这个方法只要数据库进行了升级,Room 就会将当前的数据库销毁,然后在重新创建,随之而来的副作用就是之前数据库中的所有数据就全部丢失了。
Room.databaseBuilder(context.applicationContext,
AppDatabase::class.java, "app_database")
// 只建议在开发测试阶段使用
.fallbackToDestructiveMigration()
.build()
2.需求升级——添加表
随着业务逻辑的升级,现在打算在数据库中添加一张 Book 表,那么首先要做的就是创建一个 Book 的实体类,如下所示:
@Entity
data class Book(var name: String, var pages: Int) {
@PrimaryKey(autoGenerate = true)
var id: Long = 0
}
然后创建一个 BookDao 接口,并在其中随意定义一些 API:
@Dao
interface BookDao {
@Insert
fun insertBook(book: Book): Long
@Query("SELECT * FROM Book")
fun loadAllBooks(): List<Book>
}
接下来修改 AppDatabase 中代码,在里面编写升级的逻辑,如下所示:
// 版本号升级成 2,添加 Book 实体类
@Database(version = 2, entities = [User::class, Book::class])
abstract class AppDatabase : RoomDatabase() {
...
abstract fun bookDao(): BookDao
companion object {
// 表示数据库从1升级到2就执行这个匿名类中升级逻辑
// 语句必须与Book实体类一致,否则异常
val MIGRATION_1_2 = object : Migration(1, 2) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("create table Book (id integer" +
"key autoincrement not null, name text not null," +
"pages integer not null)")
}
}
...
fun getDatabase(context: Context): AppDatabase {
instance?.let {
return it
}
return Room.databaseBuilder(context.applicationContext,
AppDatabase::class.java, "app_database")
// 将 MIGRATION_1_2 传入
.addMigrations(MIGRATION_1_2)
.build().apply {
instance = this
}
}
}
}
3.需求升级——修改表
现在 Book 表只有 ID、书名、页数这几个字段,而我们想要再加一个作者字段,代码如下所示:
@Entity
// 添加作者字段
data class Book(var name: String, var pages: Int, var author: String) {
@PrimaryKey(autoGenerate = true)
var id: Long = 0
}
修改 AppDatabase 中代码:如下所示:
@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'")
}
}
...
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
}
}
}
}
六、WorkManager
从 4.4 系统开始 AlarmManager 的触发事件由精准变为不精准,5.0 系统中加入了 JobScheduler 来处理后台任务,6.0 系统中引入了 Doze 和 App Standby 模式用于降低手机被后台唤醒的频率,从 8.0 系统开始直接禁用了 Service 的后台功能,只允许使用前台 Service。当然还有许许多多小细节的修改。
这么频繁的功能和 API 的变更,让开发者就很难受了,为了解决这个问题,Google 推出了 WorkManager 组件。
WorkManager 很适合用于处理一些要求定时执行的任务,它可以根据操作系统的版本自动选择底层是使用 AlarmManager 实现还是 JobScheduler 实现,从而降低了我们的使用成本。另外它还支持周期性任务、链式任务处理等功能,是一个非常强大的工具。
WorkManager 和 Service 并不相同,也没有直接的联系,Service 是 Android 系统的四大组件之一,它在没有被销毁的情况下是一直保持在后台运行的。而 WorkManager 只是一个处理定时任务的工具,它可以保证即使在应用程序退出甚至手机重启的情况下,之前注册的任务仍然将会得到执行,因此 WorkManager 很适合用于执行一些定期和服务器进行交互的任务,比如周期性地同步数据等等。
另外,使用 WorkManager 注册的周期性任务不能保证一定会得到执行,这并不是 BUG,而是系统为了减少电量消耗,可能会将触发时间临近的几个任务放在一起执行,这样可以大幅地减少 CPU 被唤醒的次数,从而有效延长电池的使用时间。
1.WorkManager的基本用法
使用前需要先在 app/build.gradle
文件中添加如下的依赖:
dependencies {
implementation "androidx.work:work-runtime:2.2.0"
}
WorkManager 的基本用法其实非常简单,主要分为以下 3 步:
- 定义一个后台任务,并实现具体的任务逻辑。
- 配置该后台任务的运行条件和约束信息,并构建后台任务请求。
- 将该后台任务请求传入 WorkManager 的
enqueue()
方法中,系统会在合适的时间运行。
首先,定义一个后台任务,这里创建一个 SimpleWorker
类,代码如下所示:
class SimpleWorker(context: Context, params: WorkerParameters) :
Worker(context, params) {
override fun doWork(): Result {
Log.d("SimpleWorker", "do work in SimpleWorker")
return Result.success()
}
}
后台任务的写法非常固定,每一个后台任务都必须继承自 Worker 类,并调用它唯一的构造函数。然后重写父类中的 doWork() 方法,在这个方法中编写具体的后台任务逻辑即可。
doWork() 方法不会运行在主线程中,因此可以在这里执行耗时逻辑。另外,doWork() 方法返回一个 Result 对象,用于表示任务的运行结果,成功返回 Result.success()
,失败返回 Result.failure()
。还有一个 Result.retry()
方法,它也代表着失败,只是可以结合 WorkRequest.Builder 的 setBackoffCriteria() 方法来重新执行任务。
两种构建方法:
// 构建单次运行的后台任务请求
val request = OneTimeWorkRequest.Builder(SimpleWorker::class.java)
.build()
// 构建周期性运行的后台任务请求
// 为了降低设备性能消耗,运行周期不能短于15分钟
val request = PeriodicWorkRequest.Builder(SimpleWorker::class.java,
15, TimeUnit.MINUTES).build()
// 将构建出的任务请求传入WorkManager的enqueue()中
// 系统会在合适的时间去运行
WorkManager.getInstance(context).enqueue(request)
在 MainActivity 中构建:
class MainActivity : AppCompatActivity() {
...
override fun onCreate(...) {
...
doWorkBtn.setOnClickListener {
val request = OneTimeWorkRequest.Builder(SimpleWorker::class.java)
.build()
// 由于没有添加任何约束,基本上会立即运行
WorkManager.getInstance(this).enqueue(request)
}
}
}
2.WorkManager处理复杂的任务
1.让后台任务在指定延迟后运行
只需要借助 setInitialDelay() 方法就可以了,代码如下:
var request = OneTimeWorkRequest.Builder(SimpleWorker::class.java)
// 在 5 分钟后运行
.setInitialDelay(5, TimeUnit.MINUTES)
.build()
2.取消后台任务
var request = OneTimeWorkRequest.Builder(SimpleWorker::class.java)
...
// 添加标签可以用于取消任务
.addTag("simple")
.build()
// 通过标签取消后台任务,可以取消所有含有 simple 标签的任务
WorkManager.getInstance(this).cancelAllWorkByTag("simple")
// 通过 id 取消后台任务
WorkManager.getInstance(this).cancelWorkById(request.id)
3.任务失败后重新执行任务
之前如果后台任务 doWork() 方法中返回了 Result.retry(),那么是可以结合 setBackoffCriteria() 方法来重新执行任务的,具体代码如下所示:
val request = OneTimeWorkRequest.Builder(SimpleWorker::class.java)
...
// 第二第三参数用于指定多久后重新执行任务,不能少于10秒
// 第一个参数指如果任务再次执行失败,下次重试的时间应该以什么形式延迟
// 分别有LINEAR和EXPONENTIAL两种代表线性和指数增加
.setBackoffCriteria(BackoffPolicy.LINEAR, 10, TimeUnit.SECONDS)
.build()
4.通过返回的 Result 对运行结果监听
下面演示通过 ID 对运行结果进行监听,也可以调用 getWorkInfoByTagLiveData()
方法监听同一签名下所有后台任务请求的运行结果,用法是差不多的:
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")
}
}
5.链式任务
假设这里定义了 3 个独立的后台任务:同步数据、压缩数据和上传数据。现在我们想要实现先同步、再压缩、最后上传的功能,就可以借助链式任务来实现,代码如下:
val sync = ...
val compress = ...
val upload = ...
WorkManager.getInstance(this)
// 用于开启一个链式任务
.beginWith(sync)
// 上一个任务执行成功后才会执行下一个任务
.then(compress)
.then(upload)
.enqueue()