乐趣区

关于android:解决Android开发中的痛点问题用Kotlin-Flow

前言

本文旨在通过理论业务场景论述如何应用 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,将异步操作后果以同步挂起的模式发射进来。

退出移动版