28.1 Jetpack简介
Jetpack是一个开发组件工具集,它的主要目的是帮助我们编写出更加简洁的代码,并简化我们的开发过程。Jetpack中的组件有一个特点,它们大部分不依赖于任何Android系统版本,这意味着这些组件通常是定义在AndroidX库当中的,并且拥有非常好的向下兼容性。
28.2 ViewModel
Jetpack中最重要组件之一。
implementation "androidx.lifecycle:lifecycle-extensions:2.2.0"
声明ViewModel
// 不带初始化参数
class MainViewModel: ViewModel() {
var counter = countReserved
}
// 带初始化参数的
class MainViewModel(countReserved: Int): ViewModel() {
var counter = countReserved
}
带初始化参数的ViewModel,需要借助ViewModelProvider.Factory来初始化
// 带初始化参数的ViewModel,需要借助ViewModelProvider.Factory来初始化
class MainViewModelFactory(private val countReserved: Int): ViewModelProvider.Factory {
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
return MainViewModel(countReserved) as T
}
}
使用
// 不带初始化参数的创建
viewModel = ViewModelProvider(this).get(MainViewModel::class.java)
// 带初始化参数的创建
viewModel = ViewModelProvider(this,MainViewModelFactory(countReserved)).get(MainViewModel::class.java)
viewBinding.plusOneButton.setOnClickListener {
viewModel.counter++
refreshCounter()
}
private fun refreshCounter() {
viewBinding.textView.text = viewModel.counter.toString()
}
28.3 Lifecycles-监听页面生命周期
监听Activity、Fragment的生命周期
以前用LifecycleObserver接口,注解的方式。这种方式需要代码生成或反射,会造成很大的性能开销。被废弃了。
官方建议DefaultLifecycleObserver或者LifecycleEventObserver。
新建一个自定义监听类MyLifecycleObserver
class MyLifecycleObserver: DefaultLifecycleObserver {
override fun onCreate(owner: LifecycleOwner) {
super.onCreate(owner)
Log.d("MyLifecycleObserver", "onCreate")
}
override fun onStop(owner: LifecycleOwner) {
super.onStop(owner)
Log.d("MyLifecycleObserver", "onStop")
}
override fun onDestroy(owner: LifecycleOwner) {
super.onDestroy(owner)
Log.d("MyLifecycleObserver", "onDestroy")
}
}
Activity中注册
lateinit var mLifecycleObserver: MyLifecycleObserver
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
mLifecycleObserver = MyLifecycleObserver()
lifecycle.addObserver(mLifecycleObserver)
}
28.4 LiveData
LiveData是Jetpack提供的一种响应式编程组件,它可以包含任何类型的数据,并在数据发生变化的时候通知给观察者。LiveData特别适合与ViewModel结合在一起使用,虽然它也可以单独用在别的地方,但是在绝大多数情况下,它是使用在ViewModel当中的。
1. LiveData的基础用法
class MainViewModel(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是一种可变的LiveData,它的用法很简单,主要有3种读写数据的方法,分别是getValue()、setValue()和postValue()方法。
- getValue()方法用于获取LiveData中包含的数据;
- setValue()方法用于给LiveData设置数据,但是只能在主线程中调用;
- postValue()方法用于在非主线程中给LiveData设置数据。
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
viewBinding = ActivityMainBinding.inflate(layoutInflater)
setContentView(viewBinding.root)
viewModel = ViewModelProvider(this, MainViewModelFactory(countReserved)).get(MainViewModel::class.java)
viewBinding.plusOneButton.setOnClickListener {
viewModel.plusOne()
}
viewBinding.clearButton.setOnClickListener {
viewModel.clear()
}
// 添加监听,counter有变化,就会回调到这里
viewModel.counter.observe(this, Observer {
viewBinding.textView.text = it.toString()
})
}
如果异步线程中修改LiveData,需要调用postValue(),要不然会崩溃
fun asyncTask() {
thread {
val count = viewModel.counter.value ?: 0
viewModel.counter.postValue(count + 1)
}
}
lifecycle-livedata-ktx就是一个专门为Kotlin语言设计的库,这个库在2.2.0版本中加入了对observe()方法的语法扩展。我们只需要在app/build.gradle文件中添加如下依赖:
implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.2.0"
然后就可以使用如下语法结构的observe()方法了:
viewModel.counter.observe(this) { count ->
infoText.text = count.toString()
}
但以上ViewModel中LiveData的用法还不是很规范
比较推荐的做法是,永远只暴露不可变的LiveData给外部。这样在非ViewModel中就只能观察LiveData的数据变化,而不能给LiveData设置数据。下面我们就看一下如何改造MainViewModel来实现这样的功能:
class MainViewModel(countReserved: Int): ViewModel() {
val counter: LiveData<Int>
get() = _counter
private var _counter = MutableLiveData<Int>()
init {
_counter.value = countReserved
}
fun plusOne() {
val count = _counter.value ?: 0
_counter.value = count + 1
}
fun clear() {
_counter.value = 0
}
}
2. map和switchMap
data class User(var firstName: String, var lastName: String, var age: Int)
class MapViewModel: ViewModel() {
private val userLiveData = MutableLiveData<User>()
val userName: LiveData<String> = Transformations.map(userLiveData) {
"${it.firstName} ${it.lastName}"
}
}
调用了Transformations的map()方法来对LiveData的数据类型进行转换。
object Repository {
fun getUser(userId: String): LiveData<User> {
val liveData = MutableLiveData<User>()
liveData.value = User(userId, userId, 0)
return liveData
}
}
class SwitchMapViewModel: ViewModel() {
private val userIdLiveData = MutableLiveData<String>()
val user: LiveData<User> = Transformations.switchMap(userIdLiveData) {
Repository.getUser(it)
}
fun getUser(userId: String) {
userIdLiveData.value = userId
}
}
使用
switchMapViewModel = ViewModelProvider(this).get(SwitchMapViewModel::class.java)
switchMapViewModel.user.observe(this, Observer {
viewBinding.textView.text = it.firstName
})
viewBinding.getUserButton.setOnClickListener {
val userId = (0..10000).random().toString()
Log.d("xl", userId)
switchMapViewModel.getUser(userId)
}
整体流程:
- 当外部调用MainViewModel的getUser()方法来获取用户数据时,并不会发起任何请求或者函数调用,只会将传入的userId值设置到userIdLiveData当中。
- 一旦userIdLiveData的数据发生变化,那么观察userIdLiveData的switchMap()方法就会执行,并且调用我们编写的转换函数。
- 然后在转换函数中调用Repository.getUser()方法获取真正的用户数据。
- 同时,switchMap()方法会将Repository.getUser()方法返回的LiveData对象转换成一个可观察的LiveData对象
- 对于Activity而言,只要去观察user这个LiveData对象就可以了。
由于要减少性能消耗,当Activity处于不可见状态的时候(比如手机息屏,或者被其他的Activity遮挡),如果LiveData中的数据发生了变化,是不会通知给观察者的。只有当Activity重新恢复可见状态时,才会将数据通知给观察者,而LiveData之所以能够实现这种细节的优化,依靠的还是Lifecycles组件。
还有一个小细节,如果在Activity处于不可见状态的时候,LiveData发生了多次数据变化,当Activity恢复可见状态时,只有最新的那份数据才会通知给观察者,前面的数据在这种情况下相当于已经过期了,会被直接丢弃。
28.5 Room
ORM(Object Relational Mapping)也叫对象关系映射。简单来讲,我们使用的编程语言是面向对象语言,而使用的数据库则是关系型数据库,将面向对象的语言和面向关系的数据库之间建立一种映射关系,这就是ORM了。
它赋予了我们一个强大的功能,就是可以用面向对象的思维来和数据库进行交互,绝大多数情况下不用再和SQL语句打交道了,同时也不用担心操作数据库的逻辑会让项目的整体代码变得混乱。
1. 使用Room进行增删改查
Room的整体结构。它主要由Entity、Dao和Database这3部分组成,每个部分都有明确的职责,详细说明如下。
- Entity。用于定义封装实际数据的实体类,每个实体类都会在数据库中有一张对应的表,并且表中的列是根据实体类中的字段自动生成的。
- Dao。Dao是数据访问对象的意思,通常会在这里对数据库的各项操作进行封装,在实际编程的时候,逻辑层就不需要和底层数据库打交道了,直接和Dao层进行交互即可。
- Database。用于定义数据库中的关键信息,包括数据库的版本号、包含哪些实体类以及提供Dao层的访问实例。
build.gradle(app)添加引用:
plugins {
id 'com.android.application'
id 'org.jetbrains.kotlin.android'
id 'org.jetbrains.kotlin.kapt'
}
dependencies {
implementation "androidx.lifecycle:lifecycle-extensions:2.2.0"
// Room
def room_version = "2.1.0"
implementation "androidx.room:room-runtime:$room_version"
kapt "androidx.room:room-compiler:$room_version"
}
实体类
@Entity
data class User(var firstName: String, var lastName: String, var age: Int) {
@PrimaryKey(autoGenerate = true)
var id: Long = 0
}
UserDao
@Dao
interface UserDao {
@Insert
fun insertUser(user: User): Long
@Update
fun updateUser(user: 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
}
数据库操作通常有增删改查这4种,因此Room也提供了@Insert、@Delete、@Update和@Query这4种相应的注解。
定义Database
@Database(version = 1, entities = [User::class])
abstract class AppDatabase: RoomDatabase() {
abstract fun userDao(): UserDao
companion object {
private var instant: AppDatabase? = null
@Synchronized
fun getDatabase(context: Context): AppDatabase {
instant?.let {
return it
}
return Room.databaseBuilder(
context.applicationContext,
AppDatabase::class.java,
"app_database"
).build().apply {
instant = this
}
}
}
}
在AppDatabase类的头部使用了@Database注解,并在注解中声明了数据库的版本号以及包含哪些实体类,多个实体类之间用逗号隔开即可。
Activity中使用
lateinit var userDao: UserDao
val user1 = User("Tom", "Brady", 40)
val user2 = User("Tom", "Hanks", 63)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
userDao = AppDatabase.getDatabase(this).userDao()
addData()
updateData()
deleteData()
queryData()
}
fun addData() {
viewBinding.addDataButton.setOnClickListener {
thread {
user1.id = userDao.insertUser(user1)
user2.id = userDao.insertUser(user2)
}
}
}
fun updateData() {
viewBinding.updateButton.setOnClickListener {
thread {
user1.age = (30..50).random()
userDao.updateUser(user1)
}
}
}
fun deleteData() {
viewBinding.deleteButton.setOnClickListener {
thread {
userDao.deleteUserByLastName("Hanks")
}
}
}
fun queryData() {
viewBinding.queryButton.setOnClickListener {
thread {
for (user in userDao.loadAllUsers()) {
Log.d("MainActivity", user.toString())
}
}
}
}
由于数据库操作属于耗时操作,Room默认是不允许在主线程中进行数据库操作的,因此上述代码中我们将增删改查的功能都放到了子线程中。不过为了方便测试,Room还提供了一个更加简单的方法,如下所示:
Room.databaseBuilder(context.applicationContext, AppDatabase::class.java,"app_database")
.allowMainThreadQueries()
.build()
在构建AppDatabase实例的时候,加入一个allowMainThreadQueries()方法,这样Room就允许在主线程中进行数据库操作了,这个方法建议只在测试环境下使用。
2. Room的数据库升级
比如要添加一个新的表
添加实体类
@Entity
data class Book(var name: String, var pages: Int) {
@PrimaryKey(autoGenerate = true)
var id: Long = 0
}
BookDao
@Dao
interface BookDao {
@Insert
fun insertBook(book: Book): Long
@Query("select * from Book")
fun loadAllBooks(): List<Book>
}
AppDatabase
@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 instant: AppDatabase? = null
@Synchronized
fun getDatabase(context: Context): AppDatabase {
instant?.let {
return it
}
return Room.databaseBuilder(
context.applicationContext,
AppDatabase::class.java,
"app_database"
)
.addMigrations(MIGRATION_1_2)
.build().apply {
instant = this
}
}
}
}
在companion object结构体中,我们实现了一个Migration的匿名类,并传入了1和 2这两个参数,表示当数据库版本从1升级到2的时候就执行这个匿名类中的升级逻辑。匿名类实例的变量命名也比较有讲究,这里命名成MIGRATION_1_2,可读性更高。由于我们要新增一张Book表,所以需要在migrate()方法中编写相应的建表语句。另外必须注意的是,Book表的建表语句必须和Book实体类中声明的结构完全一致,否则Room就会抛出异常。
最后在构建AppDatabase实例的时候,加入一个addMigrations()方法,并把MIGRATION_1_2传入即可。
不过,每次数据库升级并不一定都要新增一张表,也有可能是向现有的表中添加新的列。这种情况只需要使用alter语句修改表结构就可以了,我们来看一下具体的操作过程。
Book类中增加一个字段author
@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() {
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)"
)
}
}
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 instant: AppDatabase? = null
@Synchronized
fun getDatabase(context: Context): AppDatabase {
instant?.let {
return it
}
return Room.databaseBuilder(
context.applicationContext,
AppDatabase::class.java,
"app_database"
)
.addMigrations(MIGRATION_1_2, MIGRATION_2_3)
.build().apply {
instant = this
}
}
}
}
28.6 WorkManager
WorkManager很适合用于处理一些要求定时执行的任务,它可以根据操作系统的版本自动选择底层是使用AlarmManager实现还是JobScheduler实现,从而降低了我们的使用成本。另外,它还支持周期性任务、链式任务处理等功能,是一个非常强大的工具。
WorkManager和Service并不相同,也没有直接的联系:
- Service是Android系统的四大组件之一,它在没有被销毁的情况下是一直保持在后台运行的。
- WorkManager只是一个处理定时任务的工具,它可以保证即使在应用退出甚至手机重启的情况下,之前注册的任务仍然将会得到执行,因此WorkManager很适合用于执行一些定期和服务器进行交互的任务,比如周期性地同步数据,等等。
另外,使用WorkManager注册的周期性任务不能保证一定会准时执行,这并不是bug,而是系统为了减少电量消耗,可能会将触发时间临近的几个任务放在一起执行,这样可以大幅度地减少CPU被唤醒的次数,从而有效延长电池的使用时间。
1. 基本使用
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()
}
}
doWork()方法不会运行在主线程当中,因此你可以放心地在这里执行耗时逻辑,不过这里简单起见只是打印了一行日志。另外,doWork()方法要求返回一个Result对象,用于表示任务的运行结果,成功就返回Result.success(),失败就返回Result.failure()。除此之外,还有一个Result.retry()方法,它其实也代表着失败,只是可以结合WorkRequest.Builder的setBackoffCriteria()方法来重新执行任务。
Activity中使用
fun addDoWork() {
viewBinding.doWorkButton.setOnClickListener {
val request = OneTimeWorkRequest.Builder(SimpleWorker::class.java).build()
WorkManager.getInstance(this).enqueue(request)
}
}
OneTimeWorkRequest.Builder是WorkRequest.Builder的子类,用于构建单次运行的后台任务请求。WorkRequest.Builder还有另外一个子类PeriodicWorkRequest.Builder,可用于构建周期性运行的后台任务请求,但是为了降低设备性能消耗,PeriodicWorkRequest.Builder构造函数中传入的运行周期间隔不能短于15分钟。
val request = PeriodicWorkRequest.Builder(SimpleWorker::class.java, 15, TimeUnit.MINUTES).build()
2. 处理复杂任务
指定延迟时间后执行
// 指定延迟时间后执行
viewBinding.doWorkButton.setOnClickListener {
val request = OneTimeWorkRequest.Builder(SimpleWorker::class.java)
.setInitialDelay(5, TimeUnit.MINUTES) // 指定延迟时间后执行
.build()
WorkManager.getInstance(this).enqueue(request)
}
添加标签
// 添加标签
viewBinding.doWorkButton.setOnClickListener {
val request = OneTimeWorkRequest.Builder(SimpleWorker::class.java)
.addTag("simple") // 给后台任务添加标签
.build()
WorkManager.getInstance(this).enqueue(request)
}
取消任务
// 添加标签后,可以通过标签来取消后台任务请求
WorkManager.getInstance(this).cancelAllWorkByTag("simple")
// 通过id来取消后台任务请求
WorkManager.getInstance(this).cancelWorkById(request.id)
// 取消全部任务
WorkManager.getInstance(this).cancelAllWork()
Result.retry(): 那么是可以结合setBackoffCriteria()方法来重新执行任务的
// Result.retry(): 那么是可以结合setBackoffCriteria()方法来重新执行任务的
viewBinding.doWorkButton.setOnClickListener {
val request = OneTimeWorkRequest.Builder(SimpleWorker::class.java)
.setBackoffCriteria(BackoffPolicy.LINEAR, 10, TimeUnit.SECONDS)
.build()
WorkManager.getInstance(this).enqueue(request)
}
setBackoffCriteria:
- 参1:是LINEAR和EXPONENTIAL,前者代表下次重试时间以线性的方式延迟,后者代表下次重试时间以指数的方式延迟
- 参2:10: 时间最短不能少于10秒钟
- 参3:TimeUnit.SECONDS,时间格式
对后台任务的运行结果进行监听
fun addDoWorkMonitor(request: WorkRequest) {
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")
}
}
}
也可以调用getWorkInfosByTagLiveData()方法,监听同一标签名下所有后台任务请求的运行结果。
有特色的一个功能——链式任务
假设这里定义了3个独立的后台任务:同步数据、压缩数据和上传数据。现在我们想要实现先同步、再压缩、最后上传的功能,就可以借助链式任务来实现,代码示例如下:
val sync = ...
val compress = ...
val upload = ...
WorkManager.getInstance(this)
.beginWith(sync)
.then(compress)
.then(upload)
.enqueue()