通過本文可快速了解:
1.為何使用 MVI
2.為何最終考慮 SharedFlow 實現
3.repeatOnLifecycle + SharedFlow 實現 MVI 思路
為何使用 MVI
MVI 是一響應式模型,通過唯一入口入參,并從唯一出口接收結果和完成響應。
換言之,通過將 States 聚合于 MVI-Model,頁面根據回傳結果統一完成 UI 渲染,可確保
- 所獲 States 總是最新且來源可靠唯一
- 可消除 mutable 樣板代碼
“消除樣板代碼” 相信開發者深有體會。“所獲 States 總是最新且來源可靠唯一”,對此存疑,故我們繼續一探究竟。
MVI 原始理論模型
根據網傳 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 系統下存在額外性能開銷,當頁面控件展示邏輯復雜,或需頻繁刷新時,易產生掉幀現象,
改善版本 1:使用 DataBinding
考慮到 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 綁定,
改善版本 2:使用 Sealed Class 分流
根據業務場景,將原本置于 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)多種類消息只會推送最后一個,其余皆丟失,
改善版本 3:使用 SharedFlow 回推結果
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 時會重走所有,包括已消費和未消費過,視覺上給人感覺即,控件上舊數據 “一閃而過”,
這體驗并不好,
改善版本 4:通過計數防止重復回推
故此處可加個判斷 —— 如已消費,則下次 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 應運而生,
改善版本 5:將 MVI 樣板邏輯內聚
如下,通過將 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)
}
}
改善版本 6:添加 version 防止訂閱回推
前不久在 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
來源:稀土掘金