二十八、Android-探究Jetpack

65 阅读12分钟

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)
        }

整体流程:

  1. 当外部调用MainViewModel的getUser()方法来获取用户数据时,并不会发起任何请求或者函数调用,只会将传入的userId值设置到userIdLiveData当中。
  2. 一旦userIdLiveData的数据发生变化,那么观察userIdLiveData的switchMap()方法就会执行,并且调用我们编写的转换函数。
  3. 然后在转换函数中调用Repository.getUser()方法获取真正的用户数据。
  4. 同时,switchMap()方法会将Repository.getUser()方法返回的LiveData对象转换成一个可观察的LiveData对象
  5. 对于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步:

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