前情提要
在前一篇 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层
使用如下:
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
之后:
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
是一对一的,而这里是一对多的。