官方推荐 Flow 取代 LiveData,有必要吗?

本文探讨了官方推荐使用Flow代替LiveData的原因,分析了LiveData的不足,如仅在主线程更新数据、操作符有限。Flow作为协程与响应式编程的结合,支持线程切换和背压,简化复杂数据流处理。此外,文章介绍了SharedFlow和StateFlow,作为Flow的变种,分别解决多订阅者和保持最新值的需求,并提供了页面中观察的最佳实践。最后,对比Flow与LiveData的区别,帮助开发者根据项目需求选择合适的数据流组件。

前言

打开Android架构组件页面,我们可以发现一些最新发布的jetpack组件,如RoomDataStore, Paging3,DataBinding 等都支持了Flow
Google开发者账号最近也发布了几篇使用Flow的文章,比如:从 LiveData 迁移到 Kotlin 数据流
看起来官方在大力推荐使用Flow取代LiveData,那么问题来了,有必要吗?
LiveData用得好好的,有必要再学Flow吗?本文主要回答这个问题,具体包括以下内容
1.LiveData有什么不足?
2.Flow介绍以及为什么会有Flow
3.SharedFlowStateFlow的介绍与它们之间的区别

本文具体目录如下所示:

1. LiveData有什么不足?

1.1 为什么引入LiveData?

要了解LiveData的不足,我们先了解下LiveData为什么被引入

LiveData 的历史要追溯到 2017 年。彼时,观察者模式有效简化了开发,但诸如 RxJava 一类的库对新手而言有些太过复杂。为此,架构组件团队打造了 LiveData: 一个专用于 Android 的具备自主生命周期感知能力的可观察的数据存储器类。LiveData 被有意简化设计,这使得开发者很容易上手;而对于较为复杂的交互数据流场景,建议您使用 RxJava,这样两者结合的优势就发挥出来了

可以看出,LiveData就是一个简单易用的,具备感知生命周期能力的观察者模式
它使用起来非常简单,这是它的优点,也是它的不足,因为它面对比较复杂的交互数据流场景时,处理起来比较麻烦

1.2 LiveData的不足

我们上文说过LiveData结构简单,但是不够强大,它有以下不足
1.LiveData只能在主线程更新数据
2.LiveData的操作符不够强大,在处理复杂数据流时有些捉襟见肘

关于LiveData只能在主线程更新数据,有的同学可能要问,不是有postValue吗?其实postValue也是需要切换到到主线程的,如下图所示:

这意味着当我们想要更新LiveData对象时,我们会经常更改线程(工作线程→主线程),如果在修改LiveData后又要切换回到工作线程那就更麻烦了,同时postValue可能会有丢数据的问题。

2. Flow介绍

Flow 就是 Kotlin 协程与响应式编程模型结合的产物,你会发现它与 RxJava 非常像,二者之间也有相互转换的 API,使用起来非常方便。

2.1 为什么引入Flow

为什么引入Flow,我们可以从Flow解决了什么问题的角度切入

  1. LiveData不支持线程切换,所有数据转换都将在主线程上完成,有时需要频繁更改线程,面对复杂数据流时处理起来比较麻烦
  2. RxJava又有些过于麻烦了,有许多让人傻傻分不清的操作符,入门门槛较高,同时需要自己处理生命周期,在生命周期结束时取消订阅

可以看出,Flow是介于LiveDataRxJava之间的一个解决方案,它有以下特点

  • Flow 支持线程切换、背压
  • Flow 入门的门槛很低,没有那么多傻傻分不清楚的操作符
  • 简单的数据转换与操作符,如 map 等等
  • 冷数据流,不消费则不生产数据,这一点与LiveData不同:LiveData的发送端并不依赖于接收端。
  • 属于kotlin协程的一部分,可以很好的与协程基础设施结合

关于Flow的使用,比较简单,有兴趣的同学可参阅文档:Flow文档

3. SharedFlow介绍

我们上面介绍过,Flow 是冷流,什么是冷流?

  • 冷流 :只有订阅者订阅时,才开始执行发射数据流的代码。并且冷流订阅者只能是一对一的关系,当有多个不同的订阅者时,消息是重新完整发送的。也就是说对冷流而言,有多个订阅者的时候,他们各自的事件是独立的。
  • 热流:无论有没有订阅者订阅,事件始终都会发生。当 热流有多个订阅者时,热流订阅者们的关系是一对多的关系,可以与多个订阅者共享信息。

3.1 为什么引入SharedFlow

上面其实已经说得很清楚了,冷流订阅者只能是一对一的关系,当我们要实现一个流,多个订阅者的需求时(这在开发中是很常见的),就需要热流
从命名上也很容易理解,SharedFlow即共享的Flow,可以实现一对多关系,SharedFlow是一种热流

3.2 SharedFlow的使用

我们来看看SharedFlow的构造函数

public fun <T> MutableSharedFlow(
    replay: Int = 0,
    extraBufferCapacity: Int = 0,
    onBufferOverflow: BufferOverflow = BufferOverflow.SUSPEND
): MutableSharedFlow<T>

其主要有3个参数
1.replay表示当新的订阅者Collect时,发送几个已经发送过的数据给它,默认为0,即默认新订阅者不会获取以前的数据
2.extraBufferCapacity表示减去replayMutableSharedFlow还缓存多少数据,默认为0
3.onBufferOverflow表示缓存策略,即缓冲区满了之后Flow如何处理,默认为挂起

简单使用如下:

//ViewModel
val sharedFlow=MutableSharedFlow<String>()

viewModelScope.launch{
      sharedFlow.emit("Hello")
      sharedFlow.emit("SharedFlow")
}

//Activity
lifecycleScope.launch{
    viewMode.sharedFlow.collect { 
       print(it)
    }
}

3.3 将冷流转化为SharedFlow

普通flow可使用shareIn扩展方法,转化成SharedFlow

    val sharedFlow by lazy {
        flow<Int> {
        //...
        }.shareIn(viewModelScope, WhileSubscribed(500), 0)
    }

shareIn主要也有三个参数:

@param scope 共享开始时所在的协程作用域范围
@param started 控制共享的开始和结束的策略
@param replay 状态流的重播个数

started 接受以下的三个值:
1.Lazily: 当首个订阅者出现时开始,在scope指定的作用域被结束时终止。
2.Eagerly: 立即开始,而在scope指定的作用域被结束时终止。
3.WhileSubscribed: 这种情况有些复杂,后面会详细讲解

对于那些只执行一次的操作,您可以使用Lazily或者Eagerly。然而,如果您需要观察其他的流,就应该使用WhileSubscribed来实现细微但又重要的优化工作

3.4 Whilesubscribed策略

WhileSubscribed策略会在没有收集器的情况下取消上游数据流,通过shareIn运算符创建的SharedFlow会把数据暴露给视图 (View),同时也会观察来自其他层级或者是上游应用的数据流。
让这些流持续活跃可能会引起不必要的资源浪费,例如一直通过从数据库连接、硬件传感器中读取数据等等。当您的应用转而在后台运行时,您应当保持克制并中止这些协程。

public fun WhileSubscribed(
   stopTimeoutMillis: Long = 0,
   replayExpirationMillis: Long = Long.MAX_VALUE
)

如上所示,它支持两个参数:

  • 1.stopTimeoutMillis 控制一个以毫秒为单位的延迟值,指的是最后一个订阅者结束订阅与停止上游流的时间差。默认值是 0 (立即停止).这个值非常有用,因为您可能并不想因为视图有几秒钟不再监听就结束上游流。这种情况非常常见——比如当用户旋转设备时,原来的视图会先被销毁,然后数秒钟内重建。
  • 2.replayExpirationMillis表示数据重播的过时时间,如果用户离开应用太久,此时您不想让用户看到陈旧的数据,你可以用到这个参数

4. StateFlow介绍

4.1 为什么引入StateFlow

我们前面刚刚看了SharedFlow,为什么又冒出个StateFlow?
StateFlowSharedFlow 的一个比较特殊的变种,StateFlowLiveData 是最接近的,因为:

  • 1.它始终是有值的。
  • 2.它的值是唯一的。
  • 3.它允许被多个观察者共用 (因此是共享的数据流)。
  • 4.它永远只会把最新的值重现给订阅者,这与活跃观察者的数量是无关的。

可以看出,StateFlowLiveData是比较接近的,可以获取当前的值,可以想像之所以引入StateFlow就是为了替换LiveData
总结如下:
1.StateFlow继承于SharedFlow,是SharedFlow的一个特殊变种
2.StateFlowLiveData比较相近,相信之所以推出就是为了替换LiveData

4.2 StateFlow的简单使用

我们先来看看构造函数:

public fun <T> MutableStateFlow(value: T): MutableStateFlow<T> = StateFlowImpl(value ?: NULL)

1.StateFlow构造函数较为简单,只需要传入一个默认值
2.StateFlow本质上是一个replay为1,并且没有缓冲区的SharedFlow,因此第一次订阅时会先获得默认值
3.StateFlow仅在值已更新,并且值发生了变化时才会返回,即如果更新后的值没有变化,也没会回调Collect方法,这点与LiveData不同

StateFlow类似,我们也可以用stateIn将普通流转化成SharedFlow

val result: StateFlow<Result<UiState>> = someFlow
    .stateIn(
        scope = viewModelScope, 
        started = WhileSubscribed(5000), 
        initialValue = Result.Loading
    )

shareIn类似,唯一不同的时需要传入一个默认值
同时之所以WhileSubscribed中传入了5000,是为了实现等待5秒后仍然没有订阅者存在就终止协程的功能,这个方法有以下功能

  • 用户将您的应用转至后台运行,5 秒钟后所有来自其他层的数据更新会停止,这样可以节省电量。
  • 最新的数据仍然会被缓存,所以当用户切换回应用时,视图立即就可以得到数据进行渲染。
  • 订阅将被重启,新数据会填充进来,当数据可用时更新视图。
  • 在屏幕旋转时,因为重新订阅的时间在5s内,因此上游流不会中止

4.3 在页面中观察StateFlow

LiveData类似,我们也需要经常在页面中观察StateFlow
观察StateFlow需要在协程中,因此我们需要协程构建器,一般我们会使用下面几种

  1. lifecycleScope.launch : 立即启动协程,并且在本 ActivityFragment 销毁时结束协程。
  2. LaunchWhenStartedLaunchWhenResumed,它会在lifecycleOwner进入X状态之前一直等待,又在离开X状态时挂起协程


如上图所示:
1.使用launch是不安全的,在应用在后台时也会接收数据更新,可能会导致应用崩溃
2.使用launchWhenStartedlaunchWhenResumed会好一些,在后台时不会接收数据更新,但是,上游数据流会在应用后台运行期间保持活跃,因此可能浪费一定的资源

这么说来,我们使用WhileSubscribed进行的配置岂不是无效了吗?订阅者一直存在,只有页面关闭时才会取消订阅
官方推荐repeatOnLifecycle来构建协程
在某个特定的状态满足时启动协程,并且在生命周期所有者退出该状态时停止协程,如下图所示。

比如在某个Fragment的代码中:

onCreateView(...) {
    viewLifecycleOwner.lifecycleScope.launch {
        viewLifecycleOwner.lifecycle.repeatOnLifecycle(STARTED) {
            myViewModel.myUiState.collect { ... }
        }
    }
}

当这个Fragment处于STARTED状态时会开始收集流,并且在RESUMED状态时保持收集,最终在Fragment进入STOPPED状态时结束收集过程。
结合使用repeatOnLifecycle APIWhileSubscribed,可以帮助您的应用妥善利用设备资源的同时,发挥最佳性能

4.4 页面中观察Flow的最佳方式

通过ViewModel暴露数据,并在页面中获取的最佳方式是:

  • ✔️ 使用带超时参数的 WhileSubscribed 策略暴露 Flow示例 1
  • ✔️ 使用 repeatOnLifecycle 来收集数据更新。示例 2


最佳实践如上图所示,如果采用其他方式,上游数据流会被一直保持活跃,导致资源浪费
当然,如果您并不需要使用到Kotlin Flow的强大功能,就用LiveData好了 :)

5 StateFlowSharedFlow有什么区别?

从上文其实可以看出,StateFlowSharedFlow其实是挺像的,让人有些傻傻分不清,有时候也挺难选择该用哪个的

我们总结一下,它们的区别如下:

  1. SharedFlow配置更为灵活,支持配置replay,缓冲区大小等,StateFlowSharedFlow的特化版本,replay固定为1,缓冲区大小默认为0
  2. StateFlowLiveData类似,支持通过myFlow.value获取当前状态,如果有这个需求,必须使用StateFlow
  3. SharedFlow支持发出和收集重复值,而StateFlowvalue重复时,不会回调collect
  4. 对于新的订阅者,StateFlow只会重播当前最新值,SharedFlow可配置重播元素个数(默认为0,即不重播)

可以看出,StateFlow为我们做了一些默认的配置,在SharedFlow上添加了一些默认约束,这些配置可能并不符合我们的要求

  1. 它忽略重复的值,并且是不可配置的。这会带来一些问题,比如当往List中添加元素并更新时,StateFlow会认为是重复的值并忽略
  2. 它需要一个初始值,并且在开始订阅时会回调初始值,这有可能不是我们想要的
  3. 它默认是粘性的,新用户订阅会获得当前的最新值,而且是不可配置的,而SharedFlow可以修改replay

StateFlow施加在SharedFlow上的约束可能不是最适合您,如果不需要访问myFlow.value,并且享受SharedFlow的灵活性,可以选择考虑使用SharedFlow

总结

简单往往意味着不够强大,而强大又常常意味着复杂,两者往往不能兼得,软件开发过程中常常面临这种取舍。
LiveData的简单并不是它的缺点,而是它的特点。StateFlowSharedFlow更加强大,但是学习成本也显著的更高.
我们应该根据自己的需求合理选择组件的使用

  1. 如果你的数据流比较简单,不需要进行线程切换与复杂的数据变换,LiveData对你来说相信已经足够了
  2. 如果你的数据流比较复杂,需要切换线程等操作,不需要发送重复值,需要获取myFlow.valueStateFlow对你来说是个好的选择
  3. 如果你的数据流比较复杂,同时不需要获取myFlow.value,需要配置新用户订阅重播无素的个数,或者需要发送重复的值,可以考虑使用SharedFlow

参考资料

Google 推荐在 MVVM 架构中使用 Kotlin Flow
Migrate from LiveData to StateFlow and SharedFlow
从 LiveData 迁移到 Kotlin 数据流
关于kotlin中的Collections、Sequence、Channel和Flow (二)

其他资料

Android综合资料(面试题,跨端开发,小程序,KT。。。):Github

Android学习社区:Android开发交流

`StateFlow` 和 `LiveData` 都是用于在 Android 应用中观察数据变化的组件,尤其常用于 **ViewModel 到 UI 的数据通信**。虽然它们功能相似(都是“状态流”),但设计哲学、使用场景和底层机制有显著区别。 下面从多个维度详细对比 `Kotlin` 中的 `StateFlow` 与 `LiveData`。 --- ### 一、基本定义 | 类型 | 来源 | 说明 | |------|------|------| | `LiveData` | Android Jetpack(AndroidX) | 生命周期感知的可观察数据持有者 | | `StateFlow` | Kotlin Coroutines Flow | 状态共享的冷流(Cold Flow),需收集(collect)才能发射数据 | > ⚠️ 注意:`StateFlow` 是 `SharedFlow` 的一个特例,专为“状态”建模。 --- ### 二、核心区别对比 | 特性 | LiveData | StateFlow | |------|--------|----------| | 所属技术栈 | Android 架构组件 | Kotlin 协程 (kotlinx.coroutines) | | 是否生命周期感知 | ✅ 是(自动暂停/恢复观察) | ❌ 否(需要手动配合 Lifecycle 或使用 `lifecycleScope.launchWhenStarted`) | | 运行线程 | 主线程更新(postValue 可跨线程) | 可在任意线程发射(不自动切换线程) | | 数据类型 | 持有最新值(粘性事件) | 持有当前状态(必须初始化) | | 初始化要求 | 可以 null 开始 | 必须提供初始值(如 `MutableStateFlow(0)`) | | 背压处理 | 不支持 | 支持(通过缓冲策略) | | 订阅方式 | `observe(LifecycleOwner, observer)` | `collect { }`(通常在协程中) | | 多播能力 | 自带多播(多个观察者共享) | 多播(所有收集者共享同一实例) | | 内存泄漏风险 | 低(自动解绑) | 中等(若未正确绑定生命周期) | | Kotlin 友好性 | 一般(Java 兼容优先) | ✅ 极佳(原生 Kotlin API,DSL 支持) | --- ### 三、代码示例对比 #### 示例:从 ViewModel 发送用户登录状态给 UI ##### 使用 `LiveData` ```kotlin // UserViewModel.kt class UserViewModel : ViewModel() { private val _userLoggedIn = MutableLiveData<Boolean>() val userLoggedIn: LiveData<Boolean> get() = _userLoggedIn fun login() { viewModelScope.launch { // 模拟网络请求 delay(1000) _userLoggedIn.value = true } } } // Activity 中观察 viewModel.userLoggedIn.observe(this) { isLoggedIn -> if (isLoggedIn) { textView.text = "已登录" } } ``` ✅ 自动绑定生命周期,无需担心内存泄漏。 --- ##### 使用 `StateFlow` ```kotlin // UserViewModel.kt class UserViewModel : ViewModel() { private val _userLoggedIn = MutableStateFlow(false) val userLoggedIn: StateFlow<Boolean> get() = _userLoggedIn fun login() { viewModelScope.launch { // 模拟异步操作 delay(1000) _userLoggedIn.value = true } } } // Activity 中收集(必须在协程中) lifecycleScope.launchWhenStarted { viewModel.userLoggedIn.collect { isLoggedIn -> if (isLoggedIn) { textView.text = "已登录" } } } ``` ⚠️ 关键点: - `collect` 必须在协程中执行。 - 使用 `launchWhenStarted` 确保只在 `STARTED` 状态后才开始收集,避免内存泄漏或无效刷新。 - `launchWhenResumed` 更严格,只在 `RESUMED` 时运行。 --- ### 四、关键特性详解 #### 1. **初始值要求** ```kotlin val flow = MutableStateFlow("hello") // ✅ 必须传初始值 val liveData = MutableLiveData<String>() // ✅ 可以为空,后续设置 ``` 这意味着 `StateFlow` 总是有“当前状态”,而 `LiveData` 可能一开始没有值。 #### 2. **粘性事件(Sticky Event)** 两者都具有粘性:当观察者开始观察时,会立即收到最新的值。 ```kotlin // 如果之前设置了 true _liveData.value = true // 此时再 observe,仍会收到 true liveData.observe(this) { ... } // 收到 true ``` 同样地: ```kotlin _stateFlow.value = "new" // 新的 collect 会立即收到 "new" collect { value -> ... } // 收到 "new" ``` 这是与普通 `Flow` 的重要区别(普通 `Flow` 不粘性)。 #### 3. **线程安全性** - `LiveData`: `.value` 更新必须在主线程;子线程要用 `postValue()`。 - `StateFlow`: `.value` 可以在任何线程修改,但如果你在非主线程更新 UI,仍需切回主线程。 ```kotlin // StateFlow 在后台线程更新没问题 _someJobInIoDispatcher { _stateFlow.value = "updated" } ``` 但 UI 更新仍需: ```kotlin lifecycleScope.launchWhenStarted { viewModel.stateFlow.collect { state -> withContext(Dispatchers.Main) { textView.text = state } } } ``` 或者更优雅地,在 ViewModel 中就处理好线程: ```kotlin fun loadUser() { viewModelScope.launch { val user = repository.fetchUser() // IO _stateFlow.value = user.name // 自动在主线程(因为用了 viewModelScope) } } ``` --- ### 五、何时使用哪个? | 场景 | 推荐使用 | |------|-----------| | 项目使用 Kotlin + 协程 | ✅ `StateFlow` | | 已使用 RxJava | ❌ 建议过渡到 `Flow` 或保留 `Observable` | | Java 项目 | ✅ `LiveData`(Kotlin Flow 不适合 Java) | | MVVM 架构中的 UI 状态暴露 | ✅ 两者皆可,推荐 `StateFlow`(新项目) | | 需要生命周期安全自动管理 | ✅ `LiveData` 更省心;`StateFlow` 需搭配 `lifecycleScope` | | 配合 Jetpack Compose | ✅ `StateFlow` 更自然(`.collectAsState()`) | | 流式变换操作(map/filter/debounce) | ✅ `StateFlow` + `flow.transform` 更强大 | | 简单数据通知,轻量需求 | ✅ `LiveData` 足够 | --- ### 六、最佳实践建议 #### ✅ 推荐现代 Android 开发使用: ```kotlin // ViewModel class MyViewModel : ViewModel() { private val _uiState = MutableStateFlow(UserUiState()) val uiState: StateFlow<UserUiState> = _uiState fun refresh() { viewModelScope.launch { try { val userData = repository.load() _uiState.value = _uiState.value.copy(data = userData, loading = false) } catch (e: Exception) { _uiState.value = _uiState.value.copy(error = e.message, loading = false) } } } } // 在 Activity / Fragment 中 lifecycleScope.launchWhenStarted { repeatOnLifecycle(Lifecycle.State.STARTED) { viewModel.uiState.collect { state -> updateUi(state) } } } ``` 📌 使用 `repeatOnLifecycle` 是关键,确保流收集不会在后台持续运行。 --- ### 七、总结 | 维度 | LiveData | StateFlow | |------|---------|----------| | 易用性(生命周期) | ⭐⭐⭐⭐⭐ | ⭐⭐⭐(需手动控制) | | 功能丰富性 | ⭐⭐⭐ | ⭐⭐⭐⭐⭐(结合 Flow 操作符) | | Kotlin 协程集成 | ⭐⭐ | ⭐⭐⭐⭐⭐ | | 性能 | 良好 | 更高效(轻量级 Flow 实现) | | 未来趋势 | 稳定但逐渐被替代 | ✅ Google 推荐方向(尤其 Compose) | > 🔔 **Google 官方建议**:对于新的 Kotlin 项目,优先使用 `StateFlow` 或 `SharedFlow` 替代 `LiveData`。 ---
评论 2
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值