MVI 架构更佳实践:支持 LiveData 属性监听

前言

MVI架构为了解决MVVM在逻辑复杂时需要写多个LiveData(可变+不可变)的问题,使用ViewStateState集中管理,只需要订阅一个 ViewState 便可获取页面的所有状态
通过集中管理ViewState,只需对外暴露一个LiveData,解决了MVVM模式下LiveData膨胀的问题

但页面的所有状态都通过一个LiveData来管理,也带来了一个严重的问题,即页面不支持局部刷新
虽说如果是RecyclerView可以通过DifferUtil来解决,但毕竟不是所有页面都是通过RecyclerView写的,支持DifferUtil也有一定的开发成本
因此直接使用MVI架构会带来一定的性能损耗,相信这是很多人不愿意用MVI架构的原因之一

本文主要介绍如何通过监听LiveData的属性,来实现MVI架构下的局部刷新

Mavericks框架介绍

Mavericks框架是Airbnb开源的一个MVI框架,Mavericks基于Android JetpackKotlin Coroutines, 主要目标是使页面开发更高效,更容易,更有趣,目前已经在Airbnb的数百个页面上使用

下面我们来看下Mavericks是怎么使用的

// 1. 包含页面所有状态的data class
data class CounterState(val count: Int = 0) : MavericksState

// 2.负责处理业务逻辑的ViewModel,易于单元测试
class CounterViewModel(initialState: CounterState) : MavericksViewModel<CounterState>(initialState) {
    // 通过setState更新页面状态
    fun incrementCount() = setState { copy(count = count + 1) }
}

// 3. View层,必须实现MavericksView接口   
class CounterFragment : Fragment(R.layout.counter_fragment), MavericksView {
    private val viewModel: CounterViewModel by fragmentViewModel()

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        counterText.setOnClickListener {
            viewModel.incrementCount()
        }
    }

    //4. 页面刷新回调,每当状态刷新时会回调这里
    override fun invalidate() = withState(viewModel) { state ->
        counterText.text = "Count: ${state.count}"
    }
}

如上所示,看上去也很简单,主要包括几个模块

  1. 包括页面所有状态的Model层,其中的状态全都是不可变的,并且有默认值
  2. 负责处理业务逻辑的ViewModel,在其中通过setState来更新页面状态
  3. View层,必须实现MavericksView接口,每当状态刷新时都会回调invalidate函数,在这里渲染UI

可以看出,MavericksView层与Model层的交互,也并没有包装成Action,而是直接暴露的方法
上篇文章也的确有很多同学说使用Action交互比较麻烦,看起来Action这层的确可要可不要,Airbnb也没有使用,主要看个人开发习惯吧

支持局部刷新

上面介绍了Mavericks的简单使用,下面我们来看下Mavericks是怎么实现局部刷新的

data class UserState(
    val score: Int = 0,
    val previousHighScore: Int = 150,
    val livesLeft: Int = 99,
) : MavericksState {
    val pointsUntilHighScore = (previousHighScore - score).coerceAtLeast(0)
    val isHighScore = score >= previousHighScore
}

class CounterFragment : Fragment(R.layout.counter_fragment), MavericksView {

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        //直接监听State的属性,并且支持设置监听模式  
        viewModel.onEach(UserState::pointsUntilHighScore,deliveryMode = uniqueOnly()) {
            //..
        }

    viewModel.onEach(UserState::score) { 
            //...
        }    
    }
}

  1. 如上所示,Mavericks可以只监听State的其中一个属性来实现局部刷新,只有当这个属性发生变化时才触发回调
  2. onEach也可以设置监听模式,主要是为了防止数据倒灌,例如Toast这些只需要弹一次,页面重建时不应该恢复的状态,就适合使用uniqueOnly的监听模式

Mavericks实现属性监听的原理也很简单,我们一起来看下源码

fun <VM : MavericksViewModel<S>, S : MavericksState, A> VM._internal1(
    owner: LifecycleOwner?,
    prop1: KProperty1<S, A>,
    deliveryMode: DeliveryMode = RedeliverOnStart,
    action: suspend (A) -> Unit
) = stateFlow
    // 通过对象取出属性的值
    .map { MavericksTuple1(prop1.get(it)) }
    // 值发生变化了才会触发回调  
    .distinctUntilChanged()
    .resolveSubscription(owner, deliveryMode.appendPropertiesToId(prop1)) { (a) ->
        action(a)
    }

  1. 主要是通过mapState转化为它的属性值
  2. 通过distinctUntilChanged方法开启防抖,相同的值不会回调,只有值修改了才会回调
  3. 需要注意的是因为使用了KProperty1,因此State的承载数据类必须避免混淆

如上,就是Mavericks的基本介绍,想了解更多的同学可参考:github.com/airbnb/mave…

LiveData实现属性监听

上面介绍了Mavericks是怎么实现局部刷新的,但直接使用它主要有两个问题

  1. 接入起来略微有点麻烦,例如Fragment必须实现MavericksView,有一定接入成本
  2. Mavericks的局部刷新是通过Flow实现的,但相信大多数人用的还是LiveData,有一定学习成本

下面我们就来看下LiveData怎么实现属性监听

//监听一个属性
fun <T, A> LiveData<T>.observeState(
    lifecycleOwner: LifecycleOwner,
    prop1: KProperty1<T, A>,
    action: (A) -> Unit
) {
    this.map {
        StateTuple1(prop1.get(it))
    }.distinctUntilChanged().observe(lifecycleOwner) { (a) ->
        action.invoke(a)
    }
}

//监听两个属性
fun <T, A, B> LiveData<T>.observeState(
    lifecycleOwner: LifecycleOwner,
    prop1: KProperty1<T, A>,
    prop2: KProperty1<T, B>,
    action: (A, B) -> Unit
) {
    this.map {
        StateTuple2(prop1.get(it), prop2.get(it))
    }.distinctUntilChanged().observe(lifecycleOwner) { (a, b) ->
        action.invoke(a, b)
    }
}

internal data class StateTuple1<A>(val a: A)
internal data class StateTuple2<A, B>(val a: A, val b: B)

//更新State
fun <T> MutableLiveData<T>.setState(reducer: T.() -> T) {
    this.value = this.value?.reducer()
}

  1. 如上所示,主要是添加一个扩展方法,也是通过distinctUntilChanged来实现防抖
  2. 如果需要监听多个属性,例如两个属性有其中一个变化了就触发刷新,也支持传入两个属性
  3. 需要注意的是LiveData默认是不防抖的,这样改造后就是防抖的了,所以传入相同的值是不会回调的
  4. 同时需要注意下承载State的数据类需要防混淆

简单使用

上面介绍了LiveData如何实现属性监听,下面看下简单的使用

//页面状态,需要避免混淆
data class MainViewState(
    val fetchStatus: FetchStatus = FetchStatus.NotFetched,
    val newsList: List<NewsItem> = emptyList()
)

//ViewModel
class MainViewModel : ViewModel() {
    private val _viewStates: MutableLiveData<MainViewState> = MutableLiveData(MainViewState())
    //只需要暴露一个LiveData,包括页面所有状态
    val viewStates = _viewStates.asLiveData()

    private fun fetchNews() {
        //更新页面状态
        _viewStates.setState {
            copy(fetchStatus = FetchStatus.Fetching)
        }
        viewModelScope.launch {
            when (val result = repository.getMockApiResponse()) {
                //...
                is PageState.Success -> {
                    _viewStates.setState {
                        copy(fetchStatus = FetchStatus.Fetched, newsList = result.data)
                    }
                }
            }
        }
    }

}

//View层
class MainActivity : AppCompatActivity() {
    private fun initViewModel() {
        viewModel.viewStates.run {
            //监听newsList
            observeState(this@MainActivity, MainViewState::newsList) {
                newsRvAdapter.submitList(it)
            }
            //监听网络状态
            observeState(this@MainActivity, MainViewState::fetchStatus) {
                //..
            }
        }
    }
}

如上所示,其实使用起来也很简单方便

  1. ViewModel只需对外暴露一个ViewState,避免了定义多个可变不可变LiveData的问题
  2. View层支持监听LiveData的一个属性或多个属性,支持局部刷新

总结

本文主要介绍了MVI架构下如何实现局部刷新,并重点介绍了Mavericks的基本使用与原理,并在其基础上使用LiveData实现了属性监听与局部刷新
通过以上方式,解决了MVI架构的性能问题,实现了MVI架构的更佳实践

如果你的ViewModel中定义了多个可变与不可变的LiveData,就算你不使用MVI架构,支持监听LiveData属性相信也可以帮助你精简一定的代码
如果本文对你有所帮助,欢迎点赞关注Star~

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342

推荐阅读更多精彩内容