翻译自:
https://arkadiuszchmura.com/posts/be-careful-when-converting-flow-to-livedata/
前言
最近我在负责一段代码库,需要在使用 Flow
的 Data 层和仍然依赖 LiveData
暴露 State 数据的 UI 层之间实现桥接。好在 androidx.lifecycle
框架已经提供了一个叫做 asLiveData()
的方法,可以让你毫不费力地将 Flow
转为 LiveData
。
然而使用这种方式得到的 LiveData 需要牢记一点:在拥有一个及以上活跃的观察者的条件下,它才会发射数据。假使上游的 flow 产生了更新,但对应的 LiveData 并非活跃的状态,那么它将无法获得最新的数值。
让我通过如下的实例,向你展示我们可能会遇到的这种潜在问题。
示例
我们有一个简单的 Activity,它持有 AAC ViewModel
的实例:
class MainActivity : AppCompatActivity() {
private val viewModel: MainViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
}
}
该 ViewModel
的实现是这样的:
class MainViewModel : ViewModel() {
private val repository = Repository()
val state: LiveData<Int> = repository.state.asLiveData()
}
它持有一个 Repository 实例,充当琐碎的数据层。
同时 ViewModel
还通过前面提到的 asLiveData()
方法,将 Repository 持有的 StateFlow
转为了 LiveData 并对外暴露了其 State 数据。
Repository 的实现如下:
class Repository {
private val _state = MutableStateFlow(-1)
val state: StateFlow<Int> = _state
suspend fun update() {
_state.emit(Random.nextInt(until = 1000))
}
}
它拥有一个包裹着 Integer 数据(初始值为 -1)的 StateFlow
示例,同时对外提供了一个方法允许外界更新它的 State:从 0 到 1000 之间取得一个新的随机数。
试想一下,假使希望 Activity 创建的时候就能执行这个数据更新。我们可以这么实现:
- 在
MainViewModel
内创建一个init()
来做这个操作 - Activity 的
onCreate()
里调用该方法
// MainViewModel
fun init() {
// update() is suspending, so we launch a new coroutine here
viewModelScope.launch {
repository.update()
}
}
// MainActivity
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
viewModel.init()
}
这样的话,Activity 创建的时候一个新的协程将被启动,最终会调用 Repository 的 update()
,生成一个随机数并发射到它的 State。
此外,我们可能还需要在 ViewModel
中去发送包含了新生成数值的事件出去。可以在 ViewModel
中添加一个sendAnalyticalEvent()
,这样可以在执行完 Repository 的 update()
之后立即调用它。
// MainViewModel
fun init() {
viewModelScope.launch {
repository.update()
sendAnalyticalEvent() // <-- NEW
}
}
private fun sendAnalyticalEvent() {
// Typically, we would schedule a network request here
val liveDataValue = state.value
val flowValue = repository.state.value
Log.d("Current number in LiveData", "$liveDataValue")
Log.d("Current number in StateFlow", "$flowValue")
}
该方法内,我们可以做些典型的操作,比如向后端服务器发送网络请求。这里,让我们仅仅在 Logcat 里打印来自 LiveData
and Flow
的数值即可。
上面的运行结果相当出乎意料。你可能会争辩道:LiveData
没有获取到最新的数值,是因为没有足够的时间从上游的 flow 中收集数据,不然的话肯定能够拿到正确的数值。
但这个 case 里,不仅仅是 LiveData
获得到的是错误的数值,它获得到的是 null。而且请别忘了,它的存放在 Repository 里的初值是 -1。这只能代表一个意思:这里的 LiveData
压根没有从 StateFlow
里收集任何数据。
原因是我们还没有开始观察这个 LiveData
,它自然会被当作是非活跃的。而且根据 asLiveData()
方法的文档可以知道,在这种情况下 LiveData
不会从上游的 flow 收集任何数据。
asLiveData:Creates a
LiveData
that has values collected from the originFlow
.
上游 flow 数据的收集发生在
LiveData
变成活跃的时候,即LiveData.onActive
。如果 flow 尚未完成,而LiveData
变成了非激活状态,即LiveData.onActive
,那么 flow 的数据收集将在timeoutInMs
参数指定的时间后被取消。除非在超时之前,LiveData
变成活跃状态。
一旦我们开始在 Activity 里观察 LiveData
的数据(因此将促使 LiveData 变成活跃状态),它就能够拥有正确的、最新的数值了。
// MainActivity
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
viewModel.init()
viewModel.state.observe(this) { // <-- NEW
Log.d("Current number in MainActivity", "$it")
}
}
如下是 Logcat 里新的输出。
上面的示例里,我们采用的是 StateFlow
,但规则同样适用于 SharedFlow
。
而且,情况将更加糟糕,因为当 LiveData
处于非激活状态的时候,任何发送给 SharedFlow
的事件都将永久丢失(默认情况下 SharedFlow
不会将任何数值重新发送给新的订阅者)。
总结
请时刻记住采用 asLiveData()
方法转换 Flow
得到的 LiveData
将会和预期的稍稍不同:它只会在注册了活跃观察者的情况下发射数据。
就我个人而言,这种行为无可厚非:因为我们都还没有观察它、自然不会在意 LiveData
的数值是啥、能不能获取得到。但话说回来,确实存在一些场景,需要在你尚未开始观察的时候,去访问 ViewModel
中 LiveData
的当前数值。
通过阅读这篇文章,我希望你在遇到这种获取不到正确数值的情况时,不要惊讶、心中有数。