在前一篇 Android Jetpack系列之MVVM使用及封装 文章中,介绍了常用的MVC、MVP、MVVM架构及其对MVVM的封装使用,其中MVVM的主旨可以理解为数据驱动:Repository提供数据,ViewModel中发送数据,UI层使用的LiveData订阅数据,当有数据变化时会主动通知UI层进行刷新。接下来继续讨论LiveData的局限性以及google推荐的UI层订阅数据方式。
在学习LiveData时,我们知道通过LiveData可以让数据被观察,且具备生命周期感知能力,但LiveData的缺点也很明显:
· LiveData的接收只能在主线程;
· LiveData发送数据是一次性买卖,不能多次发送;
· LiveData发送数据的线程是固定的,不能切换线程,setValue/postValue本质上都是在主线程上发送的。当需要来回切换线程时,LiveData就显得无能为力了。
除了使用LiveData,还可以采用Flow替换,Flow是google官方提供的一套基于kotlin协程的响应式编程模型。常用的Flow有StateFlow、SharedFlow,详细使用参见:Android Kotlin之Flow数据流。
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层使用如下:
mViewModel.loadingFlow.flowWithLifecycle2(this, Lifecycle.State.STARTED) { isShow ->
mStatusViewUtil.showLoadingView(isShow)
}
嗯,看上去简洁了一些。
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之后:
bash
复制代码
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层是播放语音且需求是只播放一次,那么这里就会有问题了,每次切换前后台都会再播一次,不符合需求了,那么怎么办呢?接着往下看。
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改造一下,系统默认的实现如下:
@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是一对一的,而这里是一对多的。
阅读量:2013
点赞量:0
收藏量:0