Jetpack系列(二):Lifecycle与LiveData结合,打造响应式UI

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 生命周期绑定的特殊观察者。

  1. 当 Activity 被销毁时,这个观察者会自动被移除,从而释放了对 Activity 的引用,解决了内存泄露问题。

  2. 当 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 状态时,它又会重新启动一个新的协程来收集数据。

相关推荐
_祝你今天愉快3 小时前
UI核心 - UI绘制流程及原理
android
mmsx3 小时前
Android 一个设备多个 App 抢:跨进程共享服务的架构设计
android·kotlin·app
mmsx4 小时前
Android GIS系列 栅格怎么存、格网怎么归算:两个容易混淆的 GIS 链路
android·性能优化·app
陈柒吖4 小时前
安卓代码加固(1):加密DEX
android·前端
mmsx4 小时前
Android GIS系列 串口、蓝牙、USB、网络:一套统一抽象怎么扛住四种通道
android·app·产品
二流小码农4 小时前
鸿蒙开发:ArrayList,可不会让UI更新哦
android·ios·harmonyos
hai_android4 小时前
深入理解 Java 的四种引用:强引用、软引用、弱引用、虚引用
android·kotlin·android jetpack
恋猫de小郭4 小时前
Flutter Golden Tests:给 AI Agent 的 UI 测试系统
android·前端·flutter
zbmwa4 小时前
jetpack compose 副作用 SideEffect
android jetpack
知昂七昂4 小时前
AOSP编译:敲下 `lunch xxx-userdebug`,你知道系统偷偷干了什么吗?
android