Jetpack 架构组件快速入门
本文记录了 Jetpack 架构组件的核心用法,包含 ViewModel、Lifecycles、LiveData、Room、WorkManager 等关键部分。
ViewModel
ViewModel 类旨在以注重生命周期的方式存储和管理界面相关的数据。它让数据可在发生屏幕旋转等配置更改后继续留存。简单来说,ViewModel 就是将界面(Activity 或 Fragment)中显示的数据从其中分离出来,单独进行处理,减少界面的逻辑复杂程度,减轻界面的负担。
基本使用
添加依赖:
implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'
给对应的 Activity 创建一个对应的 XXXViewModel 类,并让它继承自 ViewModel(不管是 Activity 还是 Fragment 都最好给每一个都创建一个对应的 ViewModel)。
class MainViewModel : ViewModel() {
var counter = 0
}
在 Activity 中使用:
class MainActivity : AppCompatActivity() {
lateinit var viewModel: MainViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
viewModel = ViewModelProvider(this).get(viewModel::class.java)
btn_plus.setOnClickListener {
viewModel.counter++
}
refreshCounter()
}
private fun refreshCounter() {
tv_info.text = viewModel.counter.toString()
}
}
在这里需要注意,我们不能直接去创建 ViewModel 的实例,而是要通过 ViewModelProvider 来获取 ViewModel 的实例。之所以这样是因为 ViewModel 有独立的生命周期,且其生命周期长于 Activity 的生命周期。如果在 onCreate() 中创建 ViewModel 的实例,那么每次 onCreate() 执行时,ViewModel 都会创建一个实例,这样就无法保存其中的数据了。
ViewModel 对象存在的时间范围是获取 ViewModel 时传递给 ViewModelProvider 的 Lifecycle。ViewModel 将一直留在内存中,直到限定其存在时间范围的 Lifecycle 永久消失。
向 ViewModel 传递参数
借助 ViewModelProvider.Factory,我们可以实现退出程序后再打开,数据仍然不会消失的效果。
先修改 MainViewModel 的代码:
class MainViewModel(counterReserved: Int) : ViewModel() {
var counter = counterReserved
}
接着创建 MainViewModelFactory 类,并实现 ViewModelProvider.Factory 接口:
class MainViewModelFactory(private val countReserved: Int) : ViewModelProvider.Factory {
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
return MainViewModel(countReserved) as T
}
}
最后修改 Activity 中的代码:
class MainActivity : AppCompatActivity() {
lateinit var viewModel: MainViewModel
lateinit var sp: SharedPreferences
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
sp = getSharedPreferences("count_reserved", Context.MODE_PRIVATE)
val countReserved = sp.getInt("count_reserved", 0)
viewModel = ViewModelProvider(this, MainViewModelFactory(countReserved)).get(viewModel::class.java)
btn_clear.setOnClickListener {
viewModel.counter = 0
refreshCounter()
}
refreshCounter()
}
override fun onPause() {
super.onPause()
val edit = sp.edit()
edit.putInt("count_reserved", viewModel.counter)
edit.apply()
}
}
Lifecycles
顾名思义,Lifecycles 是一个用来感知 Activity 生命周期的组件。
简单使用
新建一个 MyObserver 类,并实现 LifecycleObserver 接口:
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")
}
}
这里使用 @OnLifecycleEvent 注解,并传入了一种生命周期事件。生命周期事件一共有 7 种,分别是:ON_CREATE、ON_START、ON_RESUME、ON_PAUSE、ON_STOP、ON_DESTROY、ON_ANY。
接下来需要 LifecycleOwner 去通知 MyObserver 生命周期发生了变化:
lifecycleOwner.lifecycle.addObserver(MyObserver())
大多数情况下,只要 Activity 是继承自 AppCompatActivity 的,或者 Fragment 是继承自 androidx.fragment.app.Fragment 的,那么它们本身就是一个 LifecycleOwner 的实例。
现在程序可以感知到 Activity 的生命周期变化,但没法主动获知当前的生命周期状态。解决这个问题,只需要在 MyObserver 的构造函数中将 Lifecycle 对象传进去即可:
class MyObserver(val lifecycle: Lifecycle) : LifecycleObserver { ... }
有了 Lifecycle 对象后,就可以在任何地方调用 lifecycle.currentState 来主动获取当前的生命周期状态。lifecycle.currentState 返回的生命周期状态是一个枚举类型,一共有 5 种状态类型:INITIALIZED、DESTROYED、CREATED、STARTED、RESUMED。
LiveData
LiveData 是一种可观察的数据存储器类。与常规的可观察类不同,LiveData 具有生命周期感知能力,意指它遵循其他应用组件(如 Activity、Fragment 或 Service)的生命周期。
基本使用
LiveData 可以包含任何类型的数据,并在数据发生变化时通知给观察者。
修改 MainViewModel 中的代码:
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
}
}
这里将 counter 变量修改成了一个 MutableLiveData 对象。它主要有三种读写数据的方法:
getValue():用于获取 LiveData 中包含的数据。
setValue():用于给 LiveData 设置数据,但是只能在主线程中调用。
postValue():用于在非主线程中给 LiveData 设置数据。
下面修改 MainActivity 中的代码:
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
btn_plus.setOnClickListener {
viewModel.plusOne()
}
btn_clear.setOnClickListener {
viewModel.clear()
}
viewModel.counter.observe(this, Observer { count ->
tv_info.text = count.toString()
})
}
}
关于 observe() 方法,Google 官方在专门面向 Kotlin 语言的 API 中提供了很多好用的语法扩展,要使用它需添加依赖:
implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.2.0'
之后我们就可以使用如下结构的 observe() 方法了:
viewModel.counter.observe(this) { count ->
tv_info.text = count.toString()
}
以上是 LiveData 基本用法,但仍然不是最规范的用法,主要问题是将 counter 这个可变的 LiveData 暴露给了外部。比较推荐的做法是,永远只暴露不可变的 LiveData 给外部。
class MainViewModel(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
}
}
LiveData 中的 map 和 switchMap
LiveData 提供了两种转换方法:map() 和 switchMap() 方法。
map()
这个方法的作用是将实际包含数据的 LiveData 和仅用于观察数据的 LiveData 进行转换。
class MainViewModel(countReserved: Int) : ViewModel() {
private val userLiveData = MutableLiveData<User>()
val userName: LiveData<String> = Transformations.map(userLiveData) { user ->
"${user.firstName} ${user.lastName}"
}
}
switchMap()
如果 ViewModel 中的某个 LiveData 对象是调用另外的方法获取的,那么就可以借助 switchMap() 方法。
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
}
}
Room
Room 是 Google 官方推出的一个 ORM(Object Relational Mapping 对象关系映射)框架,并将它加入了 Jetpack 中。
Room 的整体结构主要由 Entity、Dao、Database 这三个部分组成。
Room 的具体用法
添加依赖:
apply plugin: 'kotlin-kapt'
dependencies {
implementation 'androidx.room:room-runtime:2.2.5'
kapt 'androidx.room:room-compiler:2.2.5'
}
定义 Entity
@Entity
data class User(var firstName: String, var lastName: String, var age: Int) {
@PrimaryKey(autoGenerate = true)
var id: Long = 0
}
定义 Dao
@Dao
interface UserDao {
@Insert
fun insertUser(user: User): Long
@Update
fun updateUser(newUser: User)
@Query("select * from User")
fun loadAllUser(): 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
@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
}
}
}
}
Room 的数据库升级
Room 在数据库升级方面设计得比较繁琐。如果你目前还只是在开发测试阶段,不想编写那么繁琐的数据库升级逻辑,Room 提供了一个简单粗暴的方法:
Room.databaseBuilder(context.applicationContext, AppDatabase::class.java, "app_database")
.fallbackToDestructiveMigration()
.build()
下面是正规用法。我们先新建一个 Book 的实体类:
@Entity
data class Book(var name: String, var price: Int) {
@PrimaryKey(autoGenerate = true)
var id: Long = 0
}
修改 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, price integer not null)")
}
}
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")
.addMigrations(MIGRATION_1_2)
.build().apply {
instance = this
}
}
}
}
WorkManager
WorkManager 适合用于处理一些要求定时执行的任务,它可以根据操作系统的版本自动选择底层是使用 AlarmManager 实现还是 JobScheduler 实现。
WorkManager 的基本用法主要分以下三步:
- 定义一个后台任务,并实现具体的任务逻辑;
- 配置该后台任务的运行条件和约束信息,并构建后台任务请求;
- 将该后台任务请求传入
WorkManager 的 enqueue() 方法中,系统会在合适的时间运行。
基本用法
第一步,定义一个后台任务:
class SimpleWorker(context: Context, params: WorkerParameters) : Worker(context, params) {
override fun doWork(): Result {
Log.d("SimpleWorker", "do work in SimpleWorker")
return Result.success()
}
}
第二步,配置该后台任务的运行条件:
val request = OneTimeWorkRequest.Builder(SimpleWorker::class.java).build()
第三步,将构建好的后台任务请求传入 WorkManager:
WorkManager.getInstance(context).enqueue(request)
处理复杂任务
让后台任务在指定的延迟时间后运行,可以借助 setInitialDelay() 方法:
val request = PeriodicWorkRequest.Builder(SimpleWorker::class.java)
.setInitialDelay(5, TimeUnit.MINUTES)
.build()
可以给后台任务请求添加标签,并通过标签来取消后台任务请求:
var request = PeriodicWorkRequest.Builder(SimpleWorker::class.java)
.addTag("simple")
.build()
WorkManager.getInstance(this).cancelAllWorkByTag("simple")
如果想要一次性取消所有后台任务:
WorkManager.getInstance(this).cancelAllWork()
链式任务
假设定义了 3 个独立的后台任务:同步数据、压缩数据、上传数据,现在要实现先同步、再压缩、再上传的功能,就可以借助链式任务来实现:
val sync = ...
val compress = ...
val upload = ...
WorkManager.getInstance(this)
.beginWith(sync)
.then(compress)
.then(upload)
.enqueue()
注意:WorkManager 还要求,必须在前一个后台任务运行成功之后,下一个后台任务才会运行。另外,在国产手机上都有可能得不到正确的运行,因为绝大多数的国产手机厂商在进行 Android 系统定制时会增加一个一键关闭的功能,允许用户一键杀死所有非白名单的应用程序。所以千万别依赖 WorkManager 去实现什么核心功能,因为它在国产手机上可能会非常不稳定。
总结与最佳实践
Jetpack 组件极大地简化了 Android 开发流程。在使用时请注意以下几点:
- ViewModel:始终通过
ViewModelProvider 获取实例,避免手动 new。优先暴露不可变 LiveData。
- LiveData:利用
Transformations 处理数据依赖,保持数据单向流动。
- Room:数据库升级务必编写
Migration 脚本,生产环境严禁使用 fallbackToDestructiveMigration。
- WorkManager:理解其调度机制,对于强实时性任务需结合 AlarmManager 或本地广播,并注意厂商定制机的限制。
通过合理使用这些组件,可以显著提升应用的稳定性、可维护性和用户体验。