Jetpack 是一个开发组件的工具集,它的主要目的是帮助我们编写出更加简洁、规范的代码
ViewModel
传统的开发模式下,Activity 的任务太重了,既要负责逻辑处理,又要控制 UI 展示,还得处理网络回调,长此以往,项目会变得异常臃肿。ViewModel 的一个重要作用就是帮助 Activity 分担一部分工作,专门用于存放与界面相关的数据
1. 创建 ViewModel
在 app/build.gradle 文件添加依赖
dependencies {
implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'
...
}
通常来讲,比较好的规范是给每一个 Activity 和 Fragment 都创建一个对应的 ViewModel,这里就为 MainActivity 创建一个对应的 MainViewModel
class MainViewModel : ViewModel() {
var counter = 0;
}
接下来在 MainActivity 中使用这个变量
class MainActivity : AppCompatActivity() {
lateinit var viewModel: MainViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
ViewModelProvider(this).get(MainViewModel::class.java)
button.setOnClickListener {
viewModel.counter++
}
}
}
2. 向 ModelView 传递参数
如果退出程序再打开,那么之前的数据就会丢失了。因此,我们需要在退出程序时保存数据,然后重新打开程序再读取之前保存的数据,并传递给 MainModelView,因此这里修改 MainModelView 中的代码
class MainViewModel(countReserved: Int) : ViewModel() {
var counter = 0;
}
借助 ViewModelProvider.Factory 向 MainViewModel 的构造函数传递数据,新建一个 MainViewModelFactory 类,在构造函数也接收一个 countReserved 参数。另外,实现 create() 方法,在这里创建 MainViewModel 实例,并 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 的生命周期,以便在合适的时候进行相应的逻辑控制
新建一个 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
注解,并传入生命周期事件。生命周期事件的类型一共有七种:ON_CREATE、ON_START、ON_RESUME、ON_PAUSE、ON_STOP、ON_DESTROY 分别匹配 Activity 中相应的生命周期回调。另外还有一种 ON_ANY 类型,表示可以匹配 Activity 的任何生命周期回调。因此,上述代码中的方法就分别对应 Activity 的 onStart() 和 onStop() 触发执行
然后,在 MainActivity 添加一行代码,MyObserver 就能自动感知到 Activity 的生命周期了
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
lifecycle.addObserver(MyObserver())
}
}
如果希望在 MyObserver 中主动获取当前的生命周期状态,只需要在 MyObserver 的构造函数中将 Lifecycle 对象传进来即可。有了 Lifecycle 对象之后,我们就可以在任何地方调用 lifecycle.currentState 来主动获知当前的生命周期状态。lifecycle.currentState 返回的生命周期状态是一个枚举类型,一共有 INITIALIZED、DESTROYED、CREATED、STARTED、RESUMED 这五种类型,它们与 Activity 的生命周期回调所对应的关系如图
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-W8Kib0zS-1642256075778)(G:\SSS\Android\blog\lifecycle生命周期状态.jpg)]
LiveData
LiveData 是 Jetpack 提供的一种响应式编程组件,它可以包含任何类型的数据,并在数据发生变化的时候通知给观察者。LiveData 特别适合与 ViewModel 结合使用,如果我们将 ViewModel 中的数据用 LiveData 来包装,然后在 Activity 中去观察它,就可以主动将数据变化通知给 Activity 了
修改 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
}
}
MutableLiveData 是一种可变的 LiveData,主要有三种读写数据的方法,分别是 getValue()、setValue()、postValue()
- getValue() 方法用于获取 LiveData 中包含的数据
- setValue() 方法用于给 LiveData 设置数据,但只能在主线程调用
- postValue() 方法用于在非线程中给 LiveData 设置数据
接下来开始改造 MainActivity 的代码
class MainActivity : AppCompatActivity() {
lateinit var viewModel: MainViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
viewModel.counter.observe(this, Observer { count ->
...
})
}
}
调用 viewModel.counter.observe() 方法观察数据变化,该方法接收两个参数:第一个参数是一个 LifecycleOwner 对象,因此直接传 this 即可;第二个参数是一个 Observer 接口,当 counter 中包含的数据发生变化,就会回调到这里
map 和 switchMap
LiveData 为了能够应付不同的需求场景,提供了两种转换方法:map()
和 switchMap()
,提供了两种转换方法:map()
和 switchMap()
方法
先来看 map()
方法,它的作用是将实际包含数据的 LiveData
和仅用于观察数据的 LiveData
进行转换。比如有一个 User
类,包含用户的姓名和年龄,我们可以在 ViewModel
中创建创建一个 LiveData
来包含 User
类型的数据。但 MainActivity
中明确只会显示用户的姓名,而不关心用户的年龄,那么这个时候还将整个 User
类型的 LiveData
暴露给外部,就显得不那么合适了
map()
方法就是专门用于解决这种问题的,它可以将 User
类型的 LiveData
自由地转型成任意其他类型的 LiveData
class MainViewModel(countReserved: Int) : ViewModel() {
private val userLiveData = MutableLiveData<User>()
val username: LiveData<String> = Transformations.map(userLiveData) {
user -> "${user.firstName} ${user.lastName}"
}
}
这里的逻辑也很简单,就是将 User
对象转换成一个只包含用户姓名的字符串
接下来是 switchMap()
方法,前面我们所学的所有内容都有一个前提:LiveData
对象的实例都是在 ViewModel
中创建的,然而实际项目中,很有可能 ViewModel
中的某个 LiveData
对象是调用另外的方法获取的,而且这个 LiveData
对象每次都是一个新的实例,沿用以前的写法来观察,只能一直观察老的 LiveData
,从而无法观察到数据的变化
这种情况,我们可以借助 switchMap()
方法,将新的 LiveData
对象转换成另外一个可观察的 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
}
}
Room
Room 是 Android 推出的一款 ORM 框架,主要由 Entity、Dao 和 Database 三部分组成,每个部分都有明确的职责:
-
Entity
用于定义封装实际数据的实体类,每个实体类都会在数据库中有一张对应的表,表中的列是根据实体类中的字段自动生成
-
Dao
Dao 是数据访问对象,通常会在这里对数据库的各项操作进行封装
-
Database
用于定义数据库中的关键信息,包括数据库的版本号、包含哪些实体类以及提供 Dao 层的访问实例
要使用 Room,需要在 app/build.gradle 文件添加如下的依赖
apply plugin: 'kotlin-kapt'
dependencies {
...
implementation "androidx.room:room-runtimer:2.1.0"
kapt "androidx.room:room-compiler:2.1.0"
}
这里新增一个 kotlin-kapt 插件,同时在 dependencies 闭包中添加两个 Room 依赖库。由于 Room 会根据我们在项目中声明的注解动态生成代码,因此一定要使用 kapt 引入 Room 的编译时注解库,而启用编译时注解功能则一定要先添加 kotlin-kapt 插件
@Entity
data class User(val firstName: String, var lastName: String, var age: Int) {
@PrimaryKey(autoGenerate = true)
var id: Long = 0
}
我们在 User 的类名上使用 @Entity
注解,将它声明成一个实体类,然后在 User
类中添加一个 id 字段,并使用 @PrimaryKey
注解将它设为主键,再设 autoGenerate = true
,使得主键的值是自动生成
接下来看一下 Dao,新建一个 UserDao 接口
@Dao
interface UserDao {
@Insert
fun insertUser(user: User): Long
@Update
fun updateUser(user: User)
@Delete
fun deleteUser(user: User)
@Query("select * from User")
fun loadAllUsers(): List<User>
@Query("select * from User where age > :age")
fun loadUsersOlderThan(age: Int): List<User>
}
UserDao 接口使用了一个 @Dao
注解,识别成一个 Dao。Room 也提供了 @Insert
、@Delete
、@Update
、@Query
这四种注解。但是想要从数据库中查询数据,或者使用非实体类参数来增删改数据,就必须编写 SQL 语句,并使用 @Query
关于 Database
,只需要定义好三部分的内容:数据库版本号、包含哪些实体类、以及提供 Dao 层的访问实例
@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
}
}
}
}
如果要升级数据库,修改版本号,实现一个 Migration 匿名类,并传入 1 和 2 这两个参数,最后在构建实例时加入一个 addMigration() 方法,并把 MIGRATION_1_2 传入
@Database(version = 2, entities = [User::class])
abstract class AppDatabase : RoomDatabase() {
abstract fun userDao(): UserDao
companion object {
val MIGRATION_1_2 = object : Migration(1, 2) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("...")
}
}
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 适合处理一些要求定时执行的任务,它可以根据操作系统的版本自动选择合适的实现。另外,它还支持周期性任务、链式任务处理等功能
使用 WorkManager 注册的周期性任务不能保证一定会准时执行,这是系统为了减少电量消耗,可能会将触发时间临近的几个任务放在一起执行,这样可以大幅减少 CPU 被唤醒的次数
1. 基本用法
在 app/build.gradle 文件中添加如下依赖
dependencies {
...
implementation "androidx.work:work-runtime:2.2.0"
}
WorkManager 的基本用法主要分以下三步:
- 定义一个后台任务,并实现具体任务逻辑
- 配置该后台任务的运行条件和约束条件,并构建后台任务请求
- 将后台任务请求传入 WorkManager 的 enqueue() 方法,系统会在合适的时间运行
第一步,先定义一个后台任务,编写后台任务逻辑
class SimpleWorker(context: Context, params: WorkerParameters) : Worker(context, params) {
override fun doWork(): Result {
return Result.success()
}
}
第二步,配置该任务的运行条件和约束信息,这里只进行最基本的配置
// 构建单次运行的后台任务请求
val request = OneTimeWorkRequest.Builder(SimpleWorker::class.java).build()
// // 构建周期性运行的后台任务请求
val request = PeriodicWorkRequest.Builder(SimpleWorker::class.java, 15, TimeUnit.MINUTES).build()
最后一步,将构建出的后台任务请求传入 WorkManager 的 enqueue() 方法中,系统就会在合适的时间去运行
WorkManager.getInstance(context).enqueue(request)
2. 处理复杂任务
让后台任务在指定的延迟时间后运行
val request = OneTimeWorkRequest.Builder(SimpleWorker::class.java)
.setInitialDelay(5, TimeUnit.MINUTES)
.build()
给后台任务请求添加标签,最主要的一个功能就是可以通过标签来取消后台任务请求
val request = OneTimeWorkRequest.Builder(SimpleWorker::class.java)
...
.addTag("simple")
.build()
...
WorkManager.getInstance(this).cancelAllWorkByTag("simple")
如果后台任务的 doWork() 方法中返回 Result.retry(),那么可以结合 setBackoffCriteria() 方法来重新执行任务,接收三个参数:第一个参数用于指定如果任务再次执行失败,下次重试的时间应该以什么样的形式延迟;第二第三个参数用于指定在多久之后重新执行任务
val request = OneTimeWorkRequest.Builder(SimpleWorker::class.java)
...
.setBackoffCriteria(BackoffPolicy.LINEAR, 10, TimeUnit.SECOND)
.build()
对后台任务的运行结果进行监听,调用 getWorkInfoByIdLiveData() 方法,并传入后台任务请求 id,会返回一个 LiveData 对象。然后我们就可以调用 LiveData 对象的 observe() 方法观察数据变化,以此监听后台任务的运行结果
WorkManager.getInstance(context)
.getWorkInfoByIdLiveData(request.id)
.observe(this) { workInfo ->
if(workInfo.state == WorkInfo.State.SUCCEEDED) {
...
} else if(workInfo.state == WorkInfo.State.FAILED) {
...
}
}
最后再看看链式任务,定义三个独立的后台任务,依次执行
WorkManager.getInstance(context)
.beginWith(sync)
.then(compress)
.then(upload)
.enqueue(request)