前情提要
在前一篇 Android Jetpack系列之MVVM使用及封装 文章中,介绍了常用的MVC 、MVP 、MVVM 架构及其对MVVM 的封装使用,其中MVVM 的主旨可以理解为数据驱动:Repository 提供数据,ViewModel 中发送数据,UI层 使用的LiveData 订阅数据,当有数据变化时会主动通知UI层 进行刷新。接下来继续讨论LiveData 的局限性以及google 推荐的UI 层订阅数据方式。
LiveData的缺点
在学习LiveData 时,我们知道通过LiveData 可以让数据被观察,且具备生命周期感知能力,但LiveData 的缺点也很明显:
LiveData 的接收只能在主线程;LiveData 发送数据是一次性买卖,不能多次发送;LiveData 发送数据的线程是固定的,不能切换线程,setValue/postValue 本质上都是在主线程上发送的。当需要来回切换线程时,LiveData 就显得无能为力了。
除了使用LiveData ,还可以采用Flow 替换,Flow 是google 官方提供的一套基于kotlin 协程的响应式编程模型。常用的Flow 有StateFlow 、SharedFlow ,详细使用参见:Android Kotlin之Flow数据流。
Lifecycle.repeatOnLifecycle、Flow.flowWithLifecycle订阅数据
StateFlow 和 LiveData 具有相似之处。两者都是可观察的数据容器类,并且在应用架构中使用时,两者都遵循相似模式。但两者还是有不同之处的:StateFlow 需要将初始状态传递给构造函数,而 LiveData 不需要。
当 View 进入 STOPPED 状态时,LiveData.observe() 会自动取消注册使用方,而从 StateFlow 或任何其他数据流收集数据的操作并不会自动停止,即App已经切到后台了,而UI层 可能还会继续订阅数据,这样可能会存在隐患。
如需保证App 只在前台时订阅数据,需要从 Lifecycle.repeatOnLifecycle 或Flow.flowWithLifecycle 块收集数据流。google 在 使用更为安全的方式收集 Android UI 数据流中给的例子:
class LocationActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
lifecycleScope.launch {
// 单次配置任务
val expensiveObject = createExpensiveObject()
lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
// 在生命周期进入 STARTED 状态时开始重复任务,在 STOPED 状态时停止
// 对 expensiveObject 进行操作
}
// 当协程恢复时,`lifecycle` 处于 DESTROY 状态。repeatOnLifecycle 会在
// 进入 DESTROYED 状态前挂起协程的执行
}
}
}
或者
class LocationActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
locationProvider.locationFlow()
.flowWithLifecycle(this, Lifecycle.State.STARTED)
.onEach {
// 新的位置!更新地图
}
.launchIn(lifecycleScope)
}
}
其中Flow.flowWithLifecycle 内部也是通过Lifecycle.repeatOnLifecycle 实现的,上述例子中会在生命周期进入 STARTED 状态时开始重复任务,在 STOPED 状态时停止操作,如果觉得使用起来写的重复代码太多,可以简单对Flow.flowWithLifecycle 封装一下:
inline fun <T> Flow<T>.flowWithLifecycle2(
lifecycleOwner: LifecycleOwner,
minActiveState: Lifecycle.State = Lifecycle.State.STARTED,
crossinline block: suspend CoroutineScope.(T) -> Unit,
) = lifecycleOwner.lifecycleScope.launch {
//前后台切换时可以重复订阅数据。如:Lifecycle.State是STARTED,那么在生命周期进入 STARTED 状态时开始任务,在 STOPED 状态时停止订阅
flowWithLifecycle(lifecycleOwner.lifecycle, minActiveState).collect { block(it) }
}
事件分类导致的新问题
UI层 订阅的事件通常分成两种:
- 一种是同样的事件可以多次消费:比如UI的刷新,多次执行没有任何问题;
- 另一种是同样的事件只能消费一次,多次执行可能会有问题:比如
Loading 弹窗、跳转、播放音乐等。
针对第二种情况,写一个简单的例子:
//UI层
mBtnQuest.setOnClickListener {
mViewModel.getModelByFlow()
}
lifecycleScope.launch {
mViewModel.mIntFlow
.flowWithLifecycle(lifecycle, Lifecycle.State.STARTED)
.collect { value ->
log("collect here: $value")
//......其他......
}
}
//ViewModel层
private val _intFlow = MutableStateFlow<Int>(-1)
val mIntFlow = _intFlow
fun getModelByFlow() {
viewModelScope.launch {
intFlow.emit(1)
}
}
打开当前页面时,log 如下:
2022-05-08 21:34:17.775 3482-3482/org.ninetripods.mq.study E/TTT: collect here: -1
StateFlow 的默认值 -1 会先发送到UI层 ,点击Button 之后:
2022-05-08 21:34:22.921 3482-3482/org.ninetripods.mq.study E/TTT: collect here: 1
ViewModel 中发送了1并被UI层 接收。一切都很正常,此时我们把App 切到后台再切回来:
2022-05-08 21:38:01.597 3482-3482/org.ninetripods.mq.study E/TTT: collect here: 1
可以看到UI层 又接收了一遍,这是因为不管是Lifecycle.repeatOnLifecycle 或Flow.flowWithLifecycle ,切换前后台时,当Lifecycle 处于STOPED 状态,会挂起调用它的协程;并会在进入STARTED 状态时重新执行协程。如果此时UI层 是播放语音且需求是只播放一次,那么这里就会有问题了,每次切换前后台都会再播一次,不符合需求了,那么怎么办呢?接着看下一节。
避免UI层重复订阅
第一种方式:Channel
Flow 底层使用的Channel 机制实现,StateFlow、SharedFlow 都是一对多的关系,如果上游发送者与下游UI层的订阅者是一对一的关系,可以使用Channel 来实现,Channel 默认是粘性的。
Channel 使用场景:一次性消费场景,如上面说的播放背景音乐,需求是在UI层 只播一次,即使App 切到后台再切回来,也不会重复播放。Channel 使用特点:
- 每个消息只有一个订阅者可以收到,用于一对一的通信
- 第一个订阅者可以收到
collect 之前的事件,即粘性事件
Channel 使用举例:
//viewModel中
private val _loadingChannel = Channel<Boolean>()
val loadingFlow = _loadingChannel.receiveAsFlow()
private suspend fun loadStart() {
_loadingChannel.send(true)
}
private suspend fun loadFinish() {
_loadingChannel.send(false)
}
//UI层接收Loading信息
lifecycleScope.launch {
mViewModel.loadingFlow
.flowWithLifecycle(lifecycle, Lifecycle.State.STARTED)
.collect { isShow ->
mStatusViewUtil.showLoadingView(isShow)
}
}
通过Channel.receiveAsFlow() 可以将Channel 转化为Flow 使用,Channel 是一对一的关系,且下游消费完之后事件就没了,切换前后台也不会再重复消费事件了,达到了我们的要求。
第二种方式:改造Flow.flowWithLifecycle
还有一种写法,是对Flow.flowWithLifecycle 改造一下,系统默认的实现如下:
@OptIn(ExperimentalCoroutinesApi::class)
public fun <T> Flow<T>.flowWithLifecycle(
lifecycle: Lifecycle,
minActiveState: Lifecycle.State = Lifecycle.State.STARTED
): Flow<T> = callbackFlow {
lifecycle.repeatOnLifecycle(minActiveState) {
this@flowWithLifecycle.collect {
send(it)
}
}
close()
}
改为下面的方式:
/**
* NOTE: 如果不想对UI层的Lifecycle.repeatOnLifecycle/Flow.flowWithLifecycle在前后台切换时重复订阅,可以使用此方法;
* 效果类似于Channel,不过Channel是一对一的,而这里是一对多的
*/
fun <T> Flow<T>.flowOnSingleLifecycle(
lifecycle: Lifecycle,
minActiveState: Lifecycle.State = Lifecycle.State.STARTED,
isFirstCollect: Boolean = true,
): Flow<T> = callbackFlow {
var lastValue: T? = null
lifecycle.repeatOnLifecycle(minActiveState) {
this@flowOnSingleLifecycle.collect {
if ((lastValue != null || isFirstCollect) && (lastValue != it)) {
send(it)
}
lastValue = it
}
}
lastValue = null
close()
}
本质上是保存了上次的值lastValue ,如果再次订阅时会跟上次的值进行对比,只有值不一样时才会继续接收,从而达到跟Channel 类似的效果,不过Channel 是一对一的,而这里是一对多的。
|