Android Jetpack 实战


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 的基本用法主要分以下三步:

  1. 定义一个后台任务,并实现具体任务逻辑
  2. 配置该后台任务的运行条件和约束条件,并构建后台任务请求
  3. 将后台任务请求传入 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)

你可能感兴趣的:(Android,android,jetpack,android,kotlin)