通过本文可快速了解:
1.为何使用 MVI
2.为何最终考虑 SharedFlow 实现
3.repeatOnLifecycle + SharedFlow 实现 MVI 思路
MVI 是一响应式模型,通过唯一入口入参,并从唯一出口接收结果和完成响应。
换言之,通过将 States 聚合于 MVI-Model,页面根据回传结果统一完成 UI 渲染,可确保
“消除样板代码” 相信开发者深有体会。“所获 States 总是最新且来源可靠唯一”,对此存疑,故我们继续一探究竟。
根据网传 MVI 理论模型,经典 MVI 模型伪代码示例如下:
data class ViewStates(
val progress: Int,
val btnChecked: Boolean,
val title: String,
val list: List<User>,
)
class Model : Jetpack-ViewModel() {
private val _states = MutableLiveData<ViewStates>()
val states = _states.asLiveData()
fun request(intent: Intent){
when(intent){
is Intent.XXX -> {
DataRepository.xxx.onCallback{
val s = _states.getValue()
s.progress = it.progress
_states.setValue(s)
}
}
}
}
}
class View-Controller : Android-Activity() {
private val binding : ViewBinding
private val model : Model
fun onCreate(){
model.states.observe(this){
binding.progress = it.progress
binding.btnChecked = it.btnChecked
binding.tvTitle = it.title
binding.rv.adapter.refresh(it.list)
}
}
}
易得经典 MVI 模型 “牵一发动全身”,也即无论为哪个控件修改状态,所有控件皆需重刷一遍状态,
如此在 Android View 系统下存在额外性能开销,当页面控件展示逻辑复杂,或需频繁刷新时,易产生掉帧现象,
考虑到 DataBinding ObservableField 存在防抖特性,故页面可考虑 ObservableField 完成末端状态改变,尽可能消除 “控件刷新” 性能开销。
class StateHolder : Jetpack-ViewModel() {
val progress : ObservableField<Integer>()
val btnChecked : ObservableField<Boolean>()
val title : ObservableField<String>()
val list : ObservableArrayList<User>()
}
class View-Controller : Android-Activity() {
private val model : Model
private val holder : StateHolder
fun onCreate(){
model.states.observe(this){
holder.progress = it.progress
holder.btnChecked = it.btnChecked
holder.tvTitle = it.title
holder.list = it.list
}
}
}
不过,以上只是免除末端控件刷新,Observe 回调中逻辑该走还是得走,
且需开发者具备 DataBinding 使用经验、额外书写 DataBinding 样板代码和 XML 绑定,
根据业务场景,将原本置于 data class 状态分流:
sealed class ViewStates {
data class Download(var progress: Int) : ViewStates()
data class Setting(var btnChecked: Boolean) : ViewStates()
data class Info(var title: String) : ViewStates()
data class List(var list: List<User>) : ViewStates()
}
class Model : Jetpack-ViewModel() {
private val _states = MutableLiveData<ViewStates>()
val states = _states.asLiveData()
fun request(intent: Intent){
when(intent){
is Intent.XXX -> DataRepository.xxx.onCallback(_states::setValue)
}
}
}
如此可只走本次业务场景 UI 逻辑:
class View-Controller : Android-Activity() {
private val model : Model
private val holder : StateHolder
fun onCreate(){
model.states.observe(this){
when(it){
is ViewStates.Download -> holder.progress = it.progress
is ViewStates.Setting -> holder.btnChecked = it.btnChecked
is ViewStates.Info -> holder.tvTitle = it.title
is ViewStates.List -> holder.list = it.list
}
}
}
}
网上流行示例,包括官方示例,多探索和分享至此。
然实战中易得,BehaviorSubject、LiveData、StateFlow 等 replay 1 模型皆理想化 “过度设计” 产物,在生产环境中易滋生不可预期问题,
例如息屏(页面生命周期离开 STARTED)期间所获消息,replay 1 模型仅存留最后一个,那么 MVI 分流设计下,亮屏后(页面生命周期重回 STARTED)多种类消息只会推送最后一个,其余皆丢失,
SharedFlow 内有一队列,如欲亮屏后自动推送多种类消息,则可将 replay 次数设置为与队列长度一致,例如 10,
class Model : class Model : Jetpack-ViewModel() {
private val _sharedFlow: MutableSharedFlow<ViewStates>? by lazy {
MutableSharedFlow(
onBufferOverflow = BufferOverflow.DROP_OLDEST,
extraBufferCapacity = DEFAULT_QUEUE_LENGTH,
replay = DEFAULT_QUEUE_LENGTH
)
}
companion object {
private const val DEFAULT_QUEUE_LENGTH = 10
}
}
由于 replay 会重走设定次数中队列的元素,故重走 STARTED 时会重走所有,包括已消费和未消费过,视觉上给人感觉即,控件上旧数据 “一闪而过”,
这体验并不好,
故此处可加个判断 —— 如已消费,则下次 replay 时不消费。
class Model : class Model : Jetpack-ViewModel() {
private var observerCount = 0
private val _sharedFlow: MutableSharedFlow<ViewStates>? by lazy {
MutableSharedFlow(
onBufferOverflow = BufferOverflow.DROP_OLDEST,
extraBufferCapacity = DEFAULT_QUEUE_LENGTH,
replay = DEFAULT_QUEUE_LENGTH
)
}
companion object {
private const val DEFAULT_QUEUE_LENGTH = 10
}
}
data class ConsumeOnceValue<E>(
var consumeCount: Int = 0,
val value: E
)
class View-Controller : Android-Activity() {
private val model : Model
private val holder : StateHolder
fun onCreate(){
lifecycleScope?.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
model.states.collect {
if (version > currentVersion) {
if (model.consumeCount >= observerCount) return@collect
model.consumeCount++
when(it){
is ViewStates.Download -> holder.progress = it.progress
is ViewStates.Setting -> holder.btnChecked = it.btnChecked
is ViewStates.Info -> holder.tvTitle = it.title
is ViewStates.List -> holder.list = it.list
}
}
}
}
}
}
}
但每次创建一页面都需如此写一番,岂不难受,
故可将其内聚,统一抽取至单独框架维护,
MVI-Dispatcher-KTX 应运而生,
如下,通过将 repeatOnLifecycle、计数比对、mutable/immutable 等样板逻辑内聚,
open class MviDispatcherKTX<E> : ViewModel(), DefaultLifecycleObserver {
private var observerCount = 0
private val _sharedFlow: MutableSharedFlow<ConsumeOnceValue<E>>? by lazy {
MutableSharedFlow(
onBufferOverflow = BufferOverflow.DROP_OLDEST,
extraBufferCapacity = initQueueMaxLength(),
replay = initQueueMaxLength()
)
}
protected open fun initQueueMaxLength(): Int {
return DEFAULT_QUEUE_LENGTH
}
fun output(activity: AppCompatActivity?, observer: (E) -> Unit) {
observerCount++
activity?.lifecycle?.addObserver(this)
activity?.lifecycleScope?.launch {
activity.repeatOnLifecycle(Lifecycle.State.STARTED) {
_sharedFlow?.collect {
if (it.consumeCount >= observerCount) return@collect
it.consumeCount++
observer.invoke(it.value)
}
}
}
}
fun output(fragment: Fragment?, observer: (E) -> Unit) {
observerCount++
fragment?.viewLifecycleOwner?.lifecycle?.addObserver(this)
fragment?.viewLifecycleOwner?.lifecycleScope?.launch {
fragment.viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
_sharedFlow?.collect {
if (it.consumeCount >= observerCount) return@collect
it.consumeCount++
observer.invoke(it.value)
}
}
}
}
override fun onDestroy(owner: LifecycleOwner) {
super.onDestroy(owner)
observerCount--
}
protected suspend fun sendResult(event: E) {
_sharedFlow?.emit(ConsumeOnceValue(value = event))
}
fun input(event: E) {
viewModelScope.launch { onHandle(event) }
}
protected open suspend fun onHandle(event: E) {}
data class ConsumeOnceValue<E>(
var consumeCount: Int = 0,
val value: E
)
companion object {
private const val DEFAULT_QUEUE_LENGTH = 10
}
}
如此开发者哪怕不熟 MVI、mutable,只需关注 “input-output” 两处即可自动完成 “单向数据流” 开发,
class View-Controller : Android-Activity() {
private val model: MVI-Dispatcher
fun onOutput(){
model.output(this){
when(it){
is Intent.Download -> holder.progress = it.progress
is Intent.Setting -> holder.btnChecked = it.btnChecked
is Intent.Info -> holder.tvTitle = it.title
is Intent.List -> holder.list = it.list
}
}
}
fun onInput(){
model.input(Intent.Download)
}
}
前不久在 Android 开发者公众号偶遇《Jetpack MVVM 发送 Events》,文中关于 “消费且只消费一次” 描述,感觉很贴切。
且经海量样本分析易知,敏捷开发过程中,实际高频存在问题即 “消息分发一致性问题”,与其刻意区分 State 和 Event 理论概念,不如二者合而为一,升级为简明易懂 “消费且只消费一次” 线上模型。
故此处可再加个 verison 比对,
open class MviDispatcherKTX<E> : ViewModel(), DefaultLifecycleObserver {
private var version = START_VERSION
private var currentVersion = START_VERSION
private var observerCount = 0
...
fun output(activity: AppCompatActivity?, observer: (E) -> Unit) {
currentVersion = version
observerCount++
activity?.lifecycle?.addObserver(this)
activity?.lifecycleScope?.launch {
activity.repeatOnLifecycle(Lifecycle.State.STARTED) {
_sharedFlow?.collect {
if (version > currentVersion) {
if (it.consumeCount >= observerCount) return@collect
it.consumeCount++
observer.invoke(it.value)
}
}
}
}
}
protected suspend fun sendResult(event: E) {
version++
_sharedFlow?.emit(ConsumeOnceValue(value = event))
}
companion object {
private const val DEFAULT_QUEUE_LENGTH = 10
private const val START_VERSION = -1
}
}
如此便可实现 “多观察者消费且只消费一次”,解决页面初始化或息屏亮屏场景下 “Flow 错过收集” 且不滋生预期外错误:
对于 UI Event,例如通知前台弹窗、弹 Toast、页面跳转,可用该模型,
对于 UI State,例如 progress 更新,btnChecked 更新,亦可用该模型,
State 可通过 DataBinding ObservaField 或 Jetpack Compose mutableState 充当和响应,并托管于 Jetpack ViewModel,整个过程如下:
表现层 领域层 数据层
unified Event -> DomAIn Dispatcher -> Data Component
UI State/Event <- Domain Dispatcher <- Data Component
如此当页面旋屏重建时,页面自动从 Jetpack ViewModel 获取 ObservaField/mutableState 绑定和渲染控件,无需 replay 1 模型回推。
SharedFlow 仅限于 Kotlin 项目,如 JAVA 项目也想用,可参考 MVI-Dispatcher 设计,其内部维护一队列,通过基于 LiveData 改造的 Mutable-Result 亦圆满实现上述功能。
理论模型皆旨在特定环境下解决特定问题,MVI 是一理想化理论模型,直用于生产环境或滋生不可预期问题,故我们不断尝试、交流、反馈和更新。
作者:KunMinX
链接:
https://juejin.cn/post/7134594010642907149
来源:稀土掘金