Lifecycles
我们在应用中,经常需要感知 Activity 的生命周期。比如在某个界面中发起了网络请求,但当响应结果返回时,界面却关闭了,这时我们就不应该对响应结果进行处理,否则可能会导致应用崩溃。
在 Activity 内部感知其生命周期并不难,但我们常常需要在非 Activity 组件(如 ViewModel)中进行感知。
你可以通过在 Activity 中嵌入一个隐藏的 Fragment,或是通过手写监听器的方式来解决这个问题。例如,一个简单的监听器实现:
kotlin
// 手写监听器
class MyObserver {
fun activityStart() {
Log.d("MyObserver","MainActivity onStart()...")
}
fun activityStop() {
Log.d("MyObserver","MainActivity onStop()...")
}
}
class MainActivity : AppCompatActivity() {
private lateinit var observer: MyObserver
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
observer = MyObserver()
}
override fun onStart() {
super.onStart()
observer.activityStart()
}
override fun onStop() {
super.onStop()
observer.activityStop()
}
}
这种方式虽然可行,但不够优雅,会在 Activity 中引入大量模版代码,使得与业务无关的逻辑耦合在了一起。
而 Lifecycles 组件就是为了解耦出现的。它可以让任何一个普通类,都能轻松、优雅地感知 Activity 的生命周期,而不需要在 Activity 编写任何额外的分发逻辑。
我们来改造一下,新建一个 MyObserver 类,并实现 DefaultLifecycleObserver 接口。我们可以重写一系列生命周期事件的方法:
kotlin
class MyObserver : DefaultLifecycleObserver {
override fun onStart(owner: LifecycleOwner) {
// LifecycleOwner 表示拥有生命周期的对象
// 在 owner 的 onStart 回调之后调用
Log.d("MyObserver", "onStart")
}
override fun onStop(owner: LifecycleOwner) {
// 在 owner 的 onStop 回调之后调用
Log.d("MyObserver", "onStop")
}
}
那么,我们该怎么让 MyObserver 与拥有生命周期的对象(例如 Activity)关联起来呢?我们想要当 Activity 的生命周期发生变化的时候,MyObserver 能够自动接收到通知。要实现这个,我们只需借助 LifecycleOwner 接口即可。
我们可以通过 LifecycleOwner 实例的 lifecycle 属性来添加观察者:
kotlin
// 示范代码
lifecycleOwner.lifecycle.addObserver(MyObserver())
这里获取了 LifecycleOwner 实例的 Lifecycle 对象,并调用了该 Lifecycle 对象的 addObserver() 方法来注册我们的观察者。
我们的 Activity 默认继承自 AppCompatActivity,而 AppCompatActivity 已经实现了 LifecycleOwner 接口。所以 MainActivity 本身就是一个 LifecycleOwner 实例,我们可以直接在 onCreate() 方法中添加观察者:
kotlin
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
...
// 将观察者添加到生命周期中
lifecycle.addObserver(MyObserver())
}
}
当然 Fragment 也实现了
LifecycleOwner接口,也是一个LifecycleOwner实例。
可以看到,上述代码中没有任何的模版代码。并且 MyObserver 与 MainActivity 的生命周期解耦了,可以被复用到任何 LifecycleOwner 组件中。
现在,MyObserver 虽然能够被动感知 Activity 生命周期的变化,却不能主动获取当前生命周期的状态。为此,我们可以在 MyObserver 的构造函数中传入 Lifecycle 对象来实现:
kotlin
class MyObserver(private val lifecycle: Lifecycle) : DefaultLifecycleObserver {
fun doSomething() {
if (lifecycle.currentState.isAtLeast(Lifecycle.State.STARTED)) {
// 只有当生命周期处于 STARTED 或 RESUMED 状态时才会执行
Log.d("MyObserver", "Component is active. Performing action.")
}
}
}
我们可以通过 lifecycle.currentState 来获取当前的生命周期状态,一共有 INITIALIZED、DESTROYED、CREATED、STARTED、RESUMED 这五种状态。它们与 Activity 生命周期回调的对应关系如下图所示:
图片源自 Google 官方文档
LiveData
学完了 Lifecycles 之后,我们再来看一个响应式编程组件 LiveData。它可以包含任意类型的数据,并在数据发生变化时通知给观察者。绝大多数情况下,它都是配合 ViewModel 进行使用,成为 UI 与数据之间的桥梁。
LiveData 的基本用法
在 ViewModel 中,我们经常需要执行一些异步操作(如发起网络请求),当这些操作完成后才会更新数据。如果在 Activity 手动获取数据,很难保证获取的是异步操作完成后的新数据,导致界面中显示的是旧的状态。
所以,我们认为更理想的模式是:ViewModel 在数据变化后,应该主动通知 给 Activity。而这一点,我们可以使用 LiveData 来完成。
修改 MainViewModel,使用 LiveData 包装计数值:
kotlin
class MainViewModel(countReserved: Int) : ViewModel() {
// MutableLiveData 表示可变的 LiveData
private val _counter = MutableLiveData<Int>()
// 对外只暴露不可变的 LiveData,确保数据只能在 ViewModel 内部修改
val counter: LiveData<Int> = _counter
init {
// 恢复数据
_counter.value = countReserved
}
fun plusOne() {
val count = _counter.value ?: 0
_counter.value = count + 1
}
fun clear() {
_counter.value = 0
}
}
这里包含了一个很重要的最佳实践 :我们定义了一个私有的 MutableLiveData (可变) 类型的 _counter,对外暴露的则是一个 LiveData (不可变) 类型的 counter。这确保了 ViewModel 数据的封装性,在 Activity 或 Fragment 中只能观察数据,并不能修改数据,所有数据的修改逻辑都必须在 ViewModel 内部进行。
MutableLiveData 的常用方法:
-
getValue(): 获取LiveData中包含的数据,返回值可能为空。 -
setValue(T value): 设置LiveData中的数据。只能在主线程中调用,在子线程中调用会直接抛出异常。 -
postValue(T value): 用于从子线程中安全地更新数据。它内部会通过 Handler 将更新任务分发给主线程执行,所以这个更新是异步的。
然后,我们在 Activity 中观察 LiveData:
kotlin
class MainActivity : AppCompatActivity() {
// ...
override fun onCreate(savedInstanceState: Bundle?) {
// ...
binding.plusOneBtn.setOnClickListener {
viewModel.plusOne()
}
binding.clearBtn.setOnClickListener {
viewModel.clear()
}
// 调用 observe() 方法来观察数据的变化
viewModel.counter.observe(this) { count ->
// count 为最新的计数值
binding.infoText.text = "count: $count"
}
}
override fun onPause() {
super.onPause()
// 保存当前的数据
sp.edit {
putInt("count_reserved", viewModel.counter.value ?: 0)
}
}
}
运行程序,计数器还是可以正常工作。但现在我们无需手动从 ViewModel 中获取数据,LiveData 会在数据发生变化时自动将最新的数据推送给 Activity。
看到这里,你可能会觉得 LiveData 好像和 Lifecycle 并没什么关系。其实不然,LiveData 之所以能够成为 Activity 与 ViewModel 之间安全、高效的通信桥梁,靠的就是 Lifecycles 组件感知生命周期的能力。
在我们在调用 observe() 方法时,它会通过传入的 LifecycleOwner 对象创建一个与 Activity 生命周期绑定的特殊观察者。
-
当
Activity被销毁时,这个观察者会自动被移除,从而释放了对Activity的引用,解决了内存泄露问题。 -
当
Activity进入后台、对用户来说不可见时,LiveData会停止推送数据更新,避免不必要的资源消耗;只有当Activity处于前台时,LiveData才会推送最新的数据,保证数据的一致性。
这一切,都是因为这个观察者监听了 Lifecycles 的状态变化。
map 和 switchMap
LiveData 还提供了 map() 和 switchMap() 这两种转换方法,来应对更复杂的需求场景。
map 方法用于转换 LiveData 的类型。假如我们有一个 User 对象,我们不想将整个 User 对象暴露给外部。这时,我们可以使用 map() 方法。
kotlin
data class User(var firstName: String, var lastName: String, var age: Int)
class MainViewModel(countReserved: Int) : ViewModel() {
private val userLiveData = MutableLiveData<User>()
// 只将表示用户姓名的 String 类型的 LiveData 暴露给外部
val userName: LiveData<String> = userLiveData.map { user ->
// 转换逻辑
"${user.firstName} ${user.lastName}"
}
}
这样,每当 userLiveData 的数据发生变化时,map 方法就会监听到,并执行其内部的转换逻辑,然后将转换后的新数据通知给 userName 的观察者。
switchMap() 可用于解决一个 LiveData 的产生依赖于另一个 LiveData 值的场景。例如,根据可变化的 userId 来获取对应的 User 对象。
首先,定义一个 Repository,它的 getUser() 方法可以获取一个包含 User 数据的 LiveData 对象。
kotlin
object Repository {
fun getUser(userId: String): LiveData<User> {
val liveData = MutableLiveData<User>()
liveData.value = User(userId, userId, 0)
return liveData
}
}
注意:每次调用该方法都会创建一个新的 LiveData 实例。
如果我们直接观察该方法返回的 LiveData 实例,将无法在 userId 变化后,接收到新用户的更新。例如:
kotlin
// MainViewModel.kt
fun getUser(userId: String): LiveData<User> {
return Repository.getUser(userId)
}
// MainActivity.kt
viewModel.getUser(userId).observe(this){ user->
// ...
}
这时,我们可以使用 switchMap() 方法。在 MainViewModel 中添加如下内容:
kotlin
class MainViewModel(countReserved: Int) : ViewModel() {
// 用户 id
private val userIdLiveData = MutableLiveData<String>()
val user: LiveData<User> = userIdLiveData.switchMap { userId ->
Repository.getUser(userId)
}
fun searchUser(userId: String) {
// 现在只需更新用户 id 即可
userIdLiveData.value = userId
}
}
这样,每当 userIdLiveData 对象存放的用户 id 数据发生变化时,switchMap 就会执行,取消对旧 LiveData 实例的观察,并将观察的对象切换到 Repository.getUser() 方法返回的新的 LiveData 实例上。
我们来测试一下,在布局中新增一个按钮:
kotlin
<Button
android:id="@+id/searchUserBtn"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:text="Search User" />
在 MainActivity 的 onCreate() 方法中添加如下内容:
kotlin
class MainActivity : AppCompatActivity() {
...
override fun onCreate(savedInstanceState: Bundle?) {
...
// 观察固定的 LiveData 实例
viewModel.user.observe(this) { user ->
binding.infoText.text = user.firstName
}
binding.searchUserBtn.setOnClickListener {
val userId = (0..10000).random().toString()
viewModel.searchUser(userId) // 刷新用户 id 数据
}
}
...
}
运行程序,并点击 "Search User" 按钮。会发现界面中的数字一直在变化,说明我们在 MainActivity 成功观察到了数据的变化。以一种非常优雅的方式解决了动态数据源的观察问题。
最后,如果获取 LiveData 对象无需传入参数。我们只要创建一个空的 LiveData 对象,再在它的基础上调用 switchMap 方法即可。例如在下拉刷新中,用户下拉的动作就是一个触发信号。
kotlin
class MainViewModel(countReserved: Int) : ViewModel() {
// 空的 LiveData 对象
private val _refreshTrigger = MutableLiveData<Unit>()
val listData: LiveData<List<Item>> = _refreshTrigger.switchMap {
Repository.fetchNewList() // 假设该方法返回 LiveData<List<Item>> 实例
}
fun refresh() {
// 发送一个触发信号
_refreshTrigger.value = Unit
}
}
注意:我们通过设置 MutableLiveData 对象的数据来触发它的数据变化。因为 LiveData 内部并不会判断新旧数据是否相同,只要调用了 setValue() 或 postValue() 方法,就会触发数据变化事件。
StateFlow
现在,我们更多会使用 StateFlow 来代替 LiveData。因为它天生拥抱协程,并解决了 LiveData 的历史遗漏问题(如 setValue 必须在主线程调用,缺少流操作符来处理复杂数据)。
我们来改造 MainViewModel:
kotlin
class MainViewModel(countReserved: Int) : ViewModel() {
// StateFlow 对象必须要有初始值,避免了 LiveData 可能出现的空指针问题
private val _counter = MutableStateFlow(countReserved)
// 对外部暴露为不可变的 StateFlow
val counter: StateFlow<Int> = _counter.asStateFlow()
fun plusOne() {
// 使用 update 方法来更新数据,可以保证原子性操作
_counter.update { currentCount -> currentCount + 1 }
}
fun clear() {
_counter.value = 0
}
}
在 MainActivity 中,我们开启协程来收集 Flow 的数据。
kotlin
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// ...
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.counter.collect { count ->
binding.infoText.text = "count: $count"
}
}
}
}
repeatOnLifecycle 方法能够保证在 Activity 生命周期状态至少为 STARTED 时,才会收集数据,也就是说 collect 代码块才会执行。当 Activity 进入 STOPPED 状态时,它会自动取消协程;当 Activity 重新回到 STARTED 状态时,它又会重新启动一个新的协程来收集数据。