前言
本文旨在通过理论业务场景论述如何应用Kotlin Flow解决Android开发中的痛点问题,进而钻研如何优雅地应用Flow以及纠正局部典型的应用误区。无关Flow的介绍及其操作符用法能够参考:异步流 – Kotlin 语言中文站,本文不做赘述。基于LiveData+ViewModel的MVVM架构在某些场景下(以横竖屏为典型)存在局限性,本文会趁势介绍适宜Android开发的基于Flow/Channel的MVI架构。
背景
鼎力智能客户端团队在平板端鼎力一起学App上深度适配了横竖屏场景,将原先基于Rxjava的MVP架构重形成基于LiveData+ViewModel+Kotlin协程的MVVM架构。随着业务场景的复杂度晋升,LiveData作为数据的惟一载体仿佛慢慢无奈担此重任,其中一个痛点就是因为含糊了“状态”和“事件”的界线。LiveData的粘性机制会带来副作用,但这自身并不是LiveData的设计缺点,而是对它的适度应用。
Kotlin Flow是基于kotlin协程的一套异步数据流框架,能够用于异步返回多个值。kotlin 1.4.0正式版公布时推出了StateFlow和SharedFlow,两者领有Channel的很多个性,能够看作是将Flow推向台前,将Channel雪藏幕后的一手重要操作。对于新技术新框架,咱们不会自觉接入,在通过调研试用一阶段后,发现Flow的确能够为业务开发止痛提效,下文分享这个摸索的过程。
痛点一:糟糕地解决ViewModel和View层通信
发现问题
当屏幕可旋转后,LiveData不好用了?
我的项目由MVP过渡到MVVM时,其中一个典型的重构伎俩就是将Presenter中的回调写法改写成在ViewModel中持有LiveData由View层订阅,比方以下场景:
在鼎力自习室中,当老师切换至互动模式时,页面须要更改的同时还会弹出Toast提醒模式已切换。
RoomViewModel.kt
class RoomViewModel : ViewModel() {
private val _modeLiveData = MutableLiveData<Int>(-1) private val modeLiveData : LiveData<Int> = _mode fun switchMode(modeSpec : Int) { _modeLiveData.postValue(modeSpec) }
}
RoomActivity.kt
class RoomActivity : BaseActivity() {
... override fun initObserver() { roomViewModel.modeLiveData.observe(this, Observer { updateUI() showToast(it) }) }
}
这样的写法乍一看没有故障,但没有思考到横竖屏切换如果随同页面销毁重建的话,会导致在以后页面每次屏幕旋转都会从新执行observe,也就导致了每次旋转后都会弹一遍Toast。
LiveData会保障订阅者总能在值变动的时候察看到最新的值,并且每个首次订阅的观察者都会执行一次回调办法。这样的个性对于维持 UI 和数据的一致性没有任何问题,但想要察看LiveData来发射一次性的事件就超出了其能力范畴。
当然,有一种解法通过保障LiveData同一个值只会触发一次onChanged回调,封装了MutableLiveData的SingleLiveEvent。先不谈它有没有其余问题,但就其对LiveData的魔改包装给我的第一感触是强扭的瓜不甜,违反了LiveData的设计思维,其次它就没有别的问题了吗?
ViewModel和View层的通信只依赖LiveData足够吗?
在应用MVVM架构时,数据变动驱动UI更新。对于UI来说只需关怀最终状态,但对于一些事件,并不全是心愿依照LiveData的合并策略将最新一条之前的事件全副抛弃。绝大部分状况是心愿每条事件都能被执行,而LiveData并非为此设计。
在鼎力自习室中,老师会给体现好的同学点赞,收到点赞的同学会依据点赞类型弹出不同款式的点赞弹窗。为了避免横竖屏或者配置变动导致的反复弹窗,应用了下面提到的SingleLiveEvent
RoomViewModel.kt
class RoomViewModel : ViewModel() {
private val praiseEvent = SingleLiveEvent<Int>() fun recvPraise(praiseType : Int) { praiseEvent.postValue(praiseType) }
}
RoomActivity.kt
class RoomActivity : BaseActivity() {
... override fun initObserver() { roomViewModel.praiseEvent.observe(this, Observer { showPraiseDialog(it) }) }
}
思考如下状况,老师同时给同学A“坐姿端正”和“互动踊跃”两种点赞,端上预期是要别离弹两次点赞弹窗。但依据下面的实现,如果两次recvPraise在一个UI刷新周期之内间断调用,即liveData在很短的工夫内间断post两次,最终导致学生只会弹起第二个点赞的弹窗。
总的来说,上述两个问题基本都在于没有更好的伎俩去解决ViewModel和View层的通信,具体表现为对LiveData泛滥地应用以及没有对 “状态” 和 “事件” 进行辨别
剖析问题
根据上述总结,LiveData确实适宜用来示意“状态”,但“事件”不应该是由某单个值示意。想要让View层程序地生产每条事件,与此同时又不影响事件的发送,我的第一反馈是应用一个阻塞队列来承载事件。但选型时咱们要思考以下问题,也是LiveData被举荐应用的劣势 :
是否会产生内存透露,观察者的生命周期受到销毁后是否自我清理
是否反对线程切换,比方LiveData保障在主线程感知变动并更新UI
不会在观察者非沉闷状态下生产事件,比方LiveData避免因Activity进行时生产导致crash
计划一:阻塞队列
ViewModel持有阻塞队列,View层在主线程死循环读取队列内容。须要手动增加lifecycleObserver来保障线程的挂起和复原,并且不反对协程。思考应用kotlin协程中的Channel代替。
计划二: Kotlin Channel
Kotlin Channel和阻塞队列很相似,区别在于Channel用挂起的send操作代替了阻塞的put,用挂起的receive操作代替了阻塞的take。而后开启灵魂三问:
在生命周期组件中生产Channel是否会内存透露?
不会,因为Channel并不会持有生命周期组件的援用,并不像LiveData传入Observer式的应用。
是否反对线程切换?
反对,对Channel的收集须要开启协程,协程中能够切换协程上下文从而实现线程切换。
观察者非沉闷状态下是否还会生产事件?
应用lifecycle-runtime-ktx库中的launchWhenX办法,对Channel的收集协程会在组件生命周期 < X时挂起,从而防止异样。也能够应用repeatOnLifecycle(State) 来在UI层收集,当生命周期 < State时,会勾销协程,复原时再重新启动协程。
看起来应用Channel承载事件是个不错的抉择,并且一般来说事件散发都是一对一,因而并不需要反对一对多的BroadcastChannel(后者曾经逐步被废除,被SharedFlow代替)
如何创立Channel?看一下Channel对外裸露可供使用的构造方法,思考传入适合的参数。
public fun <E> Channel(
// 缓冲区容量,当超出容量时会触发onBufferOverflow指定的策略 capacity: Int = RENDEZVOUS, // 缓冲区溢出策略,默认为挂起,还有DROP_OLDEST和DROP_LATEST onBufferOverflow: BufferOverflow = BufferOverflow.SUSPEND, // 解决元素未能胜利送达解决的状况,如订阅者被勾销或者抛异样 onUndeliveredElement: ((E) -> Unit)? = null
): Channel<E>
首先Channel是热的,即任意时刻发送元素到Channel即便没有订阅者也会执行。所以思考到存在订阅者协程被勾销时发送事件的状况,即存在Channel处在无订阅者时的空档期收到事件状况。例如当Activity应用repeatOnLifecycle办法启动协程去生产ViewModel持有的Channel里的事件音讯,以后Activity因为处于STOPED状态而勾销了协程。
依据之前剖析的诉求,空档期的事件不能抛弃,而应该在Activity回到沉闷状态时顺次生产。所以思考当缓冲区溢出时策略为挂起,容量默认0即可,即默认构造方法即合乎咱们的需要。
之前咱们提到,BroadcastChannel曾经被SharedFlow代替,那咱们用Flow代替Channel是否可行呢?
计划三:一般Flow(冷流)
Flow is cold, Channel is hot。所谓流是冷的即流的结构器中的代码直到流被收集时才会执行,上面是个十分经典的例子:
fun fibonacci(): Flow<BigInteger> = flow {
var x = BigInteger.ZERO var y = BigInteger.ONE while (true) { emit(x) x = y.also { y += x } }
}
fibonacci().take(100).collect { println(it) }
如果flow结构器里的代码不依赖订阅者独立执行,下面则会间接死循环,而理论运行发现是失常输入。
那么回到咱们的问题,这里用冷流是否可行?显然并不适合,因为首先直观上冷流就无奈在结构器以外发射数据。
但实际上答案并不相对,通过在flow结构器外部应用channel,同样能够实现动静发射,如channelFlow。然而channelFlow自身不反对在结构器以外发射值,通过Channel.receiveAsFlow操作符能够将Channel转换成channelFlow。这样产生的Flow“外冷内热”,应用成果和间接收集Channel简直没有区别。
private val testChannel: Channel<Int> = Channel()
private val testChannelFlow = testChannel.receiveAsFlow ()
计划四:SharedFlow/StateFlow
首先二者都是热流,并反对在结构器外发射数据。简略看下它们的构造方法
public fun <T> MutableSharedFlow(
// 每个新的订阅者订阅时收到的回放的数目,默认0 replay: Int = 0, // 除了replay数目之外,缓存的容量,默认0 extraBufferCapacity: Int = 0, // 缓存区溢出时的策略,默认为挂起。只有当至多有一个订阅者时,onBufferOverflow才会失效。当无订阅者时,只有最近replay数目的值会保留,并且onBufferOverflow有效。 onBufferOverflow: BufferOverflow = BufferOverflow.SUSPEND
)
//MutableStateFlow等价于应用如下结构参数的SharedFlow
MutableSharedFlow(
replay = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST
)
SharedFlow被Pass的起因次要有两个:
SharedFlow反对被多个订阅者订阅,导致同一个事件会被屡次生产,并不合乎预期。
如果认为1还能够通过开发标准管制,SharedFlow的在无订阅者时会抛弃数据的个性则让其彻底无缘被选用承载必须被执行的事件
而StateFlow能够了解成非凡的SharedFlow,也就无论如何都会有下面两点问题。
当然,适宜应用SharedFlow/StateFlow的场景也有很多,下文还会重点钻研。
总结
对于想要在ViewModel层发射必须执行且只能执行一次的事件让View层执行时,不要再通过向LiveData postValue让View层监听实现。举荐应用Channel或者是通过Channel.receiveAsFlow办法创立的ChannelFlow来实现ViewModel层的事件发送。
解决问题
RoomViewModel.kt
class RoomViewModel : ViewModel() {
private val _effect = Channel<Effect> = Channel () val effect = _effect. receiveAsFlow () private fun setEffect(builder: () -> Effect) { val newEffect = builder() viewModelScope.launch { _effect.send(newEffect) } } fun showToast(text : String) { setEffect { Effect.ShowToastEffect(text) } }
}
sealed class Effect {
data class ShowToastEffect(val text: String) : Effect()
}
RoomActivity.kt
class RoomActivity : BaseActivity() {
... override fun initObserver() { lifecycleScope.launchWhenStarted { viewModel.effect.collect { when (it) { is Effect.ShowToastEffect -> { showToast(it.text) } } } } }
}
痛点二:Activity/Fragment通过共享ViewModel通信的问题
咱们常常让Activity和其中的Fragment独特持有由Acitivity作为ViewModelStoreOwner结构的ViewModel,来实现Activity和Fragment、以及Fragment之间的通信。典型场景如下:
class MyActivity : BaseActivity() {
private val viewModel : MyViewModel by viewModels() private fun initObserver() { viewModel.countLiveData.observe { it-> updateUI(it) } } private fun initListener() { button.setOnClickListener { viewModel.increaseCount() } }
}
class MyFragment : BaseFragment() {
private val activityVM : MyViewModel by activityViewModels() private fun initObserver() { activityVM.countLiveData.observe { it-> updateUI(it) } }
}
class MyViewModel : ViewModel() {
private val _countLiveData = MutableLiveData<Int>(0) private val countLiveData : LiveData<Int> = _countLiveData fun increaseCount() { _countLiveData.value = 1 + _countLiveData.value ?: 0 }
}
简略来说就是通过让Activity和Fragment察看同一个liveData,实现一致性。
那如果是要在Fragment中调用Activity的办法,通过共享ViewModel可行吗?
发现问题
DialogFragment和Activity的通信
咱们通常应用DialogFragment来实现弹窗,在其宿主Activity中设置弹窗的点击事件时,如果回调函数中援用了Activity对象,则很容易产生由横竖屏页面重建引发的援用谬误。所以咱们倡议让Activity实现接口,在弹窗每次Attach时都会将以后附着的Activity强转成接口对象来设置回调办法。
class NoticeDialogFragment : DialogFragment() {
internal lateinit var listener: NoticeDialogListener interface NoticeDialogListener { fun onDialogPositiveClick(dialog: DialogFragment) fun onDialogNegativeClick(dialog: DialogFragment) } override fun onAttach(context: Context) { super.onAttach(context) try { listener = context as NoticeDialogListener } catch (e: ClassCastException) { throw ClassCastException((context.toString() + " must implement NoticeDialogListener")) } }
}
class MainActivity : FragmentActivity(), NoticeDialogFragment.NoticeDialogListener {
fun showNoticeDialog() { val dialog = NoticeDialogFragment() dialog.show(supportFragmentManager, "NoticeDialogFragment") } override fun onDialogPositiveClick(dialog: DialogFragment) { // User touched the dialog's positive button } override fun onDialogNegativeClick(dialog: DialogFragment) { // User touched the dialog's negative button }
}
这样的写法不会有上述问题,然而随着页面上反对的弹窗变多,Activity须要实现的接口也越来越多,无论是对编码还是浏览代码都不是很敌对。那有没有机会借用共享的ViewModel做点文章?
剖析问题
咱们想要向ViewModel发送事件,并让所有依赖它的组件接管到事件。比方在FragmentA点击按键触发事件A,其宿主Activity、雷同宿主的FragmentB和FragmentA其自身都须要响应该事件。
有点像播送,且具备两个个性:
反对一对多,即一条音讯反对被多个订阅者生产
具备时效性,过期的音讯没有意义且不应该被提早生产。
看起来EventBus是一种实现办法,然而曾经有了ViewModel作为媒介再应用显然有些节约,EventBus还是更适宜跨页面、跨组件的通信。比照后面剖析的几种模型的应用,发现SharedFlow在这个场景下十分有用武之地。
SharedFlow相似BroadcastChannel,反对多个订阅者,一次发送多处生产。
SharedFlow配置灵便,如默认配置 capacity = 0, replay = 0,意味着新订阅者不会收到相似LiveData的回放。无订阅者时会间接抛弃,正合乎上述时效性事件的特点。
解决问题
class NoticeDialogFragment : DialogFragment() {
private val activityVM : MyViewModel by activityViewModels() fun initListener() { posBtn.setOnClickListener { activityVM.sendEvent(NoticeDialogPosClickEvent(textField.text)) dismiss() } negBtn.setOnClickListener { activityVM.sendEvent(NoticeDialogNegClickEvent) dismiss() } }
}
class MainActivity : FragmentActivity() {
private val viewModel : MyViewModel by viewModels() fun showNoticeDialog() { val dialog = NoticeDialogFragment() dialog.show(supportFragmentManager, "NoticeDialogFragment") } fun initObserver() { lifecycleScope.launchWhenStarted { viewModel.event.collect { when(it) { is NoticeDialogPosClickEvent -> { handleNoticePosClicked(it.text) } NoticeDialogNegClickEvent -> { handleNoticeNegClicked() } } } } }
}
class MyViewModel : ViewModel() {
private val _event: MutableSharedFlow<Event> = MutableSharedFlow () val event = _event. asSharedFlow () fun sendEvent(event: Event) { viewModelScope.launch { _event.emit(event) } }
}
这里通过lifecycleScope.launchWhenX启动协程其实并不是最佳实际,如果想要Activity在非沉闷状态下间接抛弃收到的事件,应该应用repeatOnLifecycle来管制协程的开启和勾销而非挂起。但思考到DialogFragment的存活周期是宿主Activity的子集,所以这里没有大问题。
基于Flow/Channel的MVI架构
后面讲的痛点问题,实际上是为了接下来要介绍的MVI架构抛砖引玉。而MVI架构的具体实现,也就是将上述解决方案交融到模版代码中,最大水平施展架构的劣势。
MVI是什么
所谓MVI,对应的别离是Model、View、Intent
Model: 不是MVC、MVP里M所代指的数据层,而是指表征 UI 状态的聚合对象。Model是不可变的,Model与呈现出的UI是一一对应的关系。
View:和MVC、MVP里做代指的V一样,指渲染UI的单元,能够是Activity或者View。能够接管用户的交互用意,会依据新的Model响应式地绘制UI。
Intent:不是传统的Android设计里的Intent,个别指用户与UI交互的用意,如按钮点击。Intent是扭转Model的惟一起源。
比照MVVM的区别次要在哪?
MVVM并没有束缚View层与ViewModel的交互方式,具体来说就是View层能够随便调用ViewModel中的办法,而MVI架构下ViewModel的实现对View层屏蔽,只能通过发送Intent来驱动事件。
MVVM架构并不强调对表征UI状态的Model值收敛,并且对能影响UI的值的批改能够分布在各个可被间接调用的办法外部。而MVI架构下,Intent是驱动UI变动的惟一起源,并且表征UI状态的值收敛在一个变量里。
基于Flow/Channel的MVI如何实现
形象出基类BaseViewModel
UiState是能够表征UI的Model,用StateFlow承载(也能够应用LiveData)
UiEvent是示意交互事件的Intent,用SharedFlow承载
UiEffect是事件带来除了扭转UI以外的副作用,用channelFlow承载
BaseViewModel.kt
abstract class BaseViewModel<State : UiState, Event : UiEvent, Effect : UiEffect> : ViewModel() {
/** * 初始状态 * stateFlow区别于LiveData必须有初始值 */ private val initialState: State by lazy { createInitialState() } abstract fun createInitialState(): State /** * uiState聚合页面的全副UI 状态 */ private val _uiState: MutableStateFlow<State> = MutableStateFlow(initialState) val uiState = _uiState.asStateFlow() /** * event蕴含用户与ui的交互(如点击操作),也有来自后盾的音讯(如切换自习模式) */ private val _event: MutableSharedFlow<Event> = MutableSharedFlow() val event = _event.asSharedFlow() /** * effect用作 事件带来的副作用,通常是 一次性事件 且 一对一的订阅关系 * 例如:弹Toast、导航Fragment等 */ private val _effect: Channel<Effect> = Channel() val effect = _effect.receiveAsFlow() init { subscribeEvents() } private fun subscribeEvents() { viewModelScope.launch { event.collect { handleEvent(it) } } } protected abstract fun handleEvent(event: Event) fun sendEvent(event: Event) { viewModelScope.launch { _event.emit(event) } } protected fun setState(reduce: State.() -> State) { val newState = currentState.reduce() _uiState.value = newState } protected fun setEffect(builder: () -> Effect) { val newEffect = builder() viewModelScope.launch { _effect.send(newEffect) } }
}
interface UiState
interface UiEvent
interface UiEffect
StateFlow根本等同于LiveData,区别在于StateFlow必须有初值,这也更合乎页面必须有初始状态的逻辑。个别应用data class实现UiState,页面所有元素的状态用成员变量示意。
用户交互事件用SharedFlow,具备时效性且反对一对多订阅,应用它能够解决上文提到的痛点二问题。
生产事件带来的副作用影响用ChannelFlow承载,不会失落且一对一订阅,只执行一次。应用它能够解决上文提到的痛点一问题。
协定类,定义具体业务须要的State、Event、Effect类
class NoteContract {
/** * pageTitle: 页面题目 * loadStatus: 上拉加载的状态 * refreshStatus: 下拉刷新的状态 * noteList : 备忘录列表 */ data class State( val pageTitle: String, val loadStatus: LoadStatus, val refreshStatus: RefreshStatus, val noteList: MutableList<NoteItem> ) : UiState sealed class Event : UiEvent { // 下拉刷新事件 object RefreshNoteListEvent : Event() // 上拉加载事件 object LoadMoreNoteListEvent: Event() // 增加按键点击事件 object AddingButtonClickEvent : Event() // 列表item点击事件 data class ListItemClickEvent(val item: NoteItem) : Event() // 增加项弹窗隐没事件 object AddingNoteDialogDismiss : Event() // 增加项弹窗增加确认点击事件 data class AddingNoteDialogConfirm(val title: String, val desc: String) : Event() // 增加项弹窗勾销确认点击事件 object AddingNoteDialogCanceled : Event() } sealed class Effect : UiEffect { // 弹出数据加载谬误Toast data class ShowErrorToastEffect(val text: String) : Effect() // 弹出增加项弹窗 object ShowAddNoteDialog : Effect() } sealed class LoadStatus { object LoadMoreInit : LoadStatus() object LoadMoreLoading : LoadStatus() data class LoadMoreSuccess(val hasMore: Boolean) : LoadStatus() data class LoadMoreError(val exception: Throwable) : LoadStatus() data class LoadMoreFailed(val errCode: Int) : LoadStatus() } sealed class RefreshStatus { object RefreshInit : RefreshStatus() object RefreshLoading : RefreshStatus() data class RefreshSuccess(val hasMore: Boolean) : RefreshStatus() data class RefreshError(val exception: Throwable) : RefreshStatus() data class RefreshFailed(val errCode: Int) : RefreshStatus() }
}
在生命周期组件中收集状态变动流和一次性事件流,发送用户交互事件
class NotePadActivity : BaseActivity() {
... override fun initObserver() { super.initObserver() lifecycleScope.launchWhenStarted { viewModel.uiState.collect { when (it.loadStatus) { is NoteContract.LoadStatus.LoadMoreLoading -> { adapter.loadMoreModule.loadMoreToLoading() } ... } when (it.refreshStatus) { is NoteContract.RefreshStatus.RefreshSuccess -> { adapter.setDiffNewData(it.noteList) refresh_layout.finishRefresh() if (it.refreshStatus.hasMore) { adapter.loadMoreModule.loadMoreComplete() } else { adapter.loadMoreModule.loadMoreEnd(false) } } ... } txv_title.text = it.pageTitle txv_desc.text = "${it.noteList.size}条记录" } } lifecycleScope.launchWhenStarted { viewModel.effect.collect { when (it) { is NoteContract.Effect.ShowErrorToastEffect -> { showToast(it.text) } is NoteContract.Effect.ShowAddNoteDialog -> { showAddNoteDialog() } } } } } private fun initListener() { btn_floating.setOnClickListener { viewModel.sendEvent(NoteContract.Event.AddingButtonClickEvent) } }
}
应用MVI有哪些益处
解决了上文的两个痛点。这也是我花很长的篇幅去介绍解决两个问题过程的起因。只有真的痛过才会感触到抉择适合架构的劣势。
单向数据流,任何状态的变动都来自事件,因而更容易定位出问题。
现实状况下对View层和ViewModel层做了接口隔离,更加解耦。
状态、事件从架构层面上就明确划分,便于束缚开发者写出丑陋的代码。
理论应用下来的问题
收缩的UiState,当页面复杂度进步,示意UiState的data class会重大收缩,并且因为其牵一发而动全身的特点,想要部分更新的代价很大。因而对于简单页面,能够通过拆分模块,让各个Fragment/View别离持有各自的ViewModel来拆解复杂度。
对于大部分的事件处理都只是调用办法,相比间接调用额定多了定义事件类型和直达局部的编码。
论断
架构中对SharedFlow和channelFlow的应用相对值得保留,就算不应用MVI架构,参考这里的实现也能够帮忙解决很多开发中的难题,尤其是波及横竖屏的问题。
能够抉择应用StateFlow/LiveData收敛页面全副状态,也能够拆分成多个。但更加倡议按UI组件模块拆分收敛。
跳过应用Intent,间接调用ViewModel办法也能够承受。
应用Flow还能给咱们带来什么
比Rxjava更简略,比LiveData更多的操作符
如应用flowOn操作符切换协程上下文、应用buffer、conflate操作符解决背压、应用debounce操作符实现防抖、应用combine操作符实现flow的组合等等。
比间接应用协程更简略地将基于回调的api改写成像同步代码一样的调用
应用callbackFlow,将异步操作后果以同步挂起的模式发射进来。