日日操夜夜添-日日操影院-日日草夜夜操-日日干干-精品一区二区三区波多野结衣-精品一区二区三区高清免费不卡

公告:魔扣目錄網為廣大站長提供免費收錄網站服務,提交前請做好本站友鏈:【 網站目錄:http://www.ylptlb.cn 】, 免友鏈快審服務(50元/站),

點擊這里在線咨詢客服
新站提交
  • 網站:51998
  • 待審:31
  • 小程序:12
  • 文章:1030137
  • 會員:747

通過本文可快速了解:

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
來源:稀土掘金

分享到:
標簽:Android
用戶無頭像

網友整理

注冊時間:

網站:5 個   小程序:0 個  文章:12 篇

  • 51998

    網站

  • 12

    小程序

  • 1030137

    文章

  • 747

    會員

趕快注冊賬號,推廣您的網站吧!
最新入駐小程序

數獨大挑戰2018-06-03

數獨一種數學游戲,玩家需要根據9

答題星2018-06-03

您可以通過答題星輕松地創建試卷

全階人生考試2018-06-03

各種考試題,題庫,初中,高中,大學四六

運動步數有氧達人2018-06-03

記錄運動步數,積累氧氣值。還可偷

每日養生app2018-06-03

每日養生,天天健康

體育訓練成績評定2018-06-03

通用課目體育訓練成績評定