通過本文可快速了解:
1.為何使用 MVI
2.為何最終考慮 SharedFlow 實現(xiàn)
3.repeatOnLifecycle + SharedFlow 實現(xiàn) MVI 思路
為何使用 MVI
MVI 是一響應(yīng)式模型,通過唯一入口入?yún)ⅲ奈ㄒ怀隹诮邮战Y(jié)果和完成響應(yīng)。
換言之,通過將 States 聚合于 MVI-Model,頁面根據(jù)回傳結(jié)果統(tǒng)一完成 UI 渲染,可確保
- 所獲 States 總是最新且來源可靠唯一
- 可消除 mutable 樣板代碼
“消除樣板代碼” 相信開發(fā)者深有體會。“所獲 States 總是最新且來源可靠唯一”,對此存疑,故我們繼續(xù)一探究竟。
MVI 原始理論模型
根據(jù)網(wǎng)傳 MVI 理論模型,經(jīng)典 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)
}
}
}
易得經(jīng)典 MVI 模型 “牽一發(fā)動全身”,也即無論為哪個控件修改狀態(tài),所有控件皆需重刷一遍狀態(tài),
如此在 Android View 系統(tǒng)下存在額外性能開銷,當頁面控件展示邏輯復雜,或需頻繁刷新時,易產(chǎn)生掉幀現(xiàn)象,
改善版本 1:使用 DataBinding
考慮到 DataBinding ObservableField 存在防抖特性,故頁面可考慮 ObservableField 完成末端狀態(tài)改變,盡可能消除 “控件刷新” 性能開銷。
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 回調(diào)中邏輯該走還是得走,
且需開發(fā)者具備 DataBinding 使用經(jīng)驗、額外書寫 DataBinding 樣板代碼和 XML 綁定,
改善版本 2:使用 Sealed Class 分流
根據(jù)業(yè)務(wù)場景,將原本置于 data class 狀態(tài)分流:
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)
}
}
}
如此可只走本次業(yè)務(wù)場景 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
}
}
}
}
網(wǎng)上流行示例,包括官方示例,多探索和分享至此。
然實戰(zhàn)中易得,BehaviorSubject、LiveData、StateFlow 等 replay 1 模型皆理想化 “過度設(shè)計” 產(chǎn)物,在生產(chǎn)環(huán)境中易滋生不可預(yù)期問題,
例如息屏(頁面生命周期離開 STARTED)期間所獲消息,replay 1 模型僅存留最后一個,那么 MVI 分流設(shè)計下,亮屏后(頁面生命周期重回 STARTED)多種類消息只會推送最后一個,其余皆丟失,
改善版本 3:使用 SharedFlow 回推結(jié)果
SharedFlow 內(nèi)有一隊列,如欲亮屏后自動推送多種類消息,則可將 replay 次數(shù)設(shè)置為與隊列長度一致,例如 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 會重走設(shè)定次數(shù)中隊列的元素,故重走 STARTED 時會重走所有,包括已消費和未消費過,視覺上給人感覺即,控件上舊數(shù)據(jù) “一閃而過”,
這體驗并不好,
改善版本 4:通過計數(shù)防止重復回推
故此處可加個判斷 —— 如已消費,則下次 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
}
}
}
}
}
}
}
但每次創(chuàng)建一頁面都需如此寫一番,豈不難受,
故可將其內(nèi)聚,統(tǒng)一抽取至單獨框架維護,
MVI-Dispatcher-KTX 應(yīng)運而生,
改善版本 5:將 MVI 樣板邏輯內(nèi)聚
如下,通過將 repeatOnLifecycle、計數(shù)比對、mutable/immutable 等樣板邏輯內(nèi)聚,
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
}
}
如此開發(fā)者哪怕不熟 MVI、mutable,只需關(guān)注 “input-output” 兩處即可自動完成 “單向數(shù)據(jù)流” 開發(fā),
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 開發(fā)者公眾號偶遇《Jetpack MVVM 發(fā)送 Events》,文中關(guān)于 “消費且只消費一次” 描述,感覺很貼切。
且經(jīng)海量樣本分析易知,敏捷開發(fā)過程中,實際高頻存在問題即 “消息分發(fā)一致性問題”,與其刻意區(qū)分 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
}
}
如此便可實現(xiàn) “多觀察者消費且只消費一次”,解決頁面初始化或息屏亮屏場景下 “Flow 錯過收集” 且不滋生預(yù)期外錯誤:
對于 UI Event,例如通知前臺彈窗、彈 Toast、頁面跳轉(zhuǎn),可用該模型,
對于 UI State,例如 progress 更新,btnChecked 更新,亦可用該模型,
State 可通過 DataBinding ObservaField 或 Jetpack Compose mutableState 充當和響應(yīng),并托管于 Jetpack ViewModel,整個過程如下:
表現(xiàn)層 領(lǐng)域?qū)? 數(shù)據(jù)層
unified Event -> Domain Dispatcher -> Data Component
UI State/Event <- Domain Dispatcher <- Data Component
如此當頁面旋屏重建時,頁面自動從 Jetpack ViewModel 獲取 ObservaField/mutableState 綁定和渲染控件,無需 replay 1 模型回推。
SharedFlow 僅限于 Kotlin 項目,如 JAVA 項目也想用,可參考 MVI-Dispatcher 設(shè)計,其內(nèi)部維護一隊列,通過基于 LiveData 改造的 Mutable-Result 亦圓滿實現(xiàn)上述功能。
綜上
理論模型皆旨在特定環(huán)境下解決特定問題,MVI 是一理想化理論模型,直用于生產(chǎn)環(huán)境或滋生不可預(yù)期問題,故我們不斷嘗試、交流、反饋和更新。
作者:KunMinX
鏈接:
https://juejin.cn/post/7134594010642907149
來源:稀土掘金