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 实例。

可以看到,上述代码中没有任何的模版代码。并且 MyObserverMainActivity 的生命周期解耦了,可以被复用到任何 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 来获取当前的生命周期状态,一共有 INITIALIZEDDESTROYEDCREATEDSTARTEDRESUMED 这五种状态。它们与 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 数据的封装性,在 ActivityFragment 中只能观察数据,并不能修改数据,所有数据的修改逻辑都必须在 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 之所以能够成为 ActivityViewModel 之间安全、高效的通信桥梁,靠的就是 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" />

MainActivityonCreate() 方法中添加如下内容:

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

相关推荐
江太翁几秒前
mediapipe流水线分析 三
android·mediapipe
与火星的孩子对话30 分钟前
Unity进阶课程【六】Android、ios、Pad 终端设备打包局域网IP调试、USB调试、性能检测、控制台打印日志等、C#
android·unity·ios·c#·ip
tmacfrank2 小时前
Android 网络全栈攻略(四)—— TCPIP 协议族与 HTTPS 协议
android·网络·https
fundroid3 小时前
Kotlin 协程:Channel 与 Flow 深度对比及 Channel 使用指南
android·kotlin·协程
草字3 小时前
cocos 打包安卓
android
DeBuggggggg4 小时前
centos 7.6安装mysql8
android
浩浩测试一下5 小时前
渗透信息收集- Web应用漏洞与指纹信息收集以及情报收集
android·前端·安全·web安全·网络安全·安全架构
移动开发者1号6 小时前
深入理解原子类与CAS无锁编程:原理、实战与优化
android·kotlin
陈卓4106 小时前
MySQL-主从复制&分库分表
android·mysql·adb
移动开发者1号6 小时前
深入理解 ThreadLocal:原理、实战与优化指南
android·kotlin