前面我们介绍了MVI
架构的基本原理与使用:MVVM 进阶版:MVI 架构了解一下~
MVI
架构为了解决MVVM
在逻辑复杂时需要写多个LiveData
(可变+不可变)的问题,使用ViewState
对State
集中管理,只需要订阅一个 ViewState
便可获取页面的所有状态
通过集中管理ViewState
,只需对外暴露一个LiveData
,解决了MVVM
模式下LiveData
膨胀的问题
但页面的所有状态都通过一个LiveData
来管理,也带来了一个严重的问题,即页面不支持局部刷新
虽说如果是RecyclerView
可以通过DifferUtil
来解决,但毕竟不是所有页面都是通过RecyclerView
写的,支持DifferUtil
也有一定的开发成本
因此直接使用MVI
架构会带来一定的性能损耗,相信这是很多人不愿意用MVI
架构的原因之一
本文主要介绍如何通过监听LiveData
的属性,来实现MVI
架构下的局部刷新
Mavericks
框架介绍Mavericks框架是Airbnb
开源的一个MVI
框架,Mavericks
基于Android Jetpack
与Kotlin Coroutines
, 主要目标是使页面开发更高效,更容易,更有趣,目前已经在Airbnb
的数百个页面上使用
下面我们来看下Mavericks
是怎么使用的
// 1. 包含页面所有状态的data class
data class CounterState(val count: Int = 0) : MavericksState
// 2.负责处理业务逻辑的ViewModel,易于单元测试
class CounterViewModel(initialState: CounterState) : MavericksViewModel(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}"
}
}
如上所示,看上去也很简单,主要包括几个模块
Model
层,其中的状态全都是不可变的,并且有默认值ViewModel
,在其中通过setState
来更新页面状态View
层,必须实现MavericksView
接口,每当状态刷新时都会回调invalidate
函数,在这里渲染UI
可以看出,Mavericks
中View
层与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) {
//...
}
}
}
Mavericks
可以只监听State
的其中一个属性来实现局部刷新,只有当这个属性发生变化时才触发回调onEach
也可以设置监听模式,主要是为了防止数据倒灌,例如Toast
这些只需要弹一次,页面重建时不应该恢复的状态,就适合使用uniqueOnly
的监听模式Mavericks
实现属性监听的原理也很简单,我们一起来看下源码
fun , S : MavericksState, A> VM._internal1(
owner: LifecycleOwner?,
prop1: KProperty1,
deliveryMode: DeliveryMode = RedeliverOnStart,
action: suspend (A) -> Unit
) = stateFlow
// 通过对象取出属性的值
.map { MavericksTuple1(prop1.get(it)) }
// 值发生变化了才会触发回调
.distinctUntilChanged()
.resolveSubscription(owner, deliveryMode.appendPropertiesToId(prop1)) { (a) ->
action(a)
}
map
将State
转化为它的属性值distinctUntilChanged
方法开启防抖,相同的值不会回调,只有值修改了才会回调KProperty1
,因此State
的承载数据类必须避免混淆如上,就是Mavericks
的基本介绍,想了解更多的同学可参考:github.com/airbnb/mave…
LiveData
实现属性监听上面介绍了Mavericks
是怎么实现局部刷新的,但直接使用它主要有两个问题
Fragment
必须实现MavericksView
,有一定接入成本Mavericks
的局部刷新是通过Flow
实现的,但相信大多数人用的还是LiveData
,有一定学习成本下面我们就来看下LiveData
怎么实现属性监听
//监听一个属性
fun LiveData.observeState(
lifecycleOwner: LifecycleOwner,
prop1: KProperty1,
action: (A) -> Unit
) {
this.map {
StateTuple1(prop1.get(it))
}.distinctUntilChanged().observe(lifecycleOwner) { (a) ->
action.invoke(a)
}
}
//监听两个属性
fun LiveData.observeState(
lifecycleOwner: LifecycleOwner,
prop1: KProperty1,
prop2: KProperty1,
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(val a: A)
internal data class StateTuple2(val a: A, val b: B)
//更新State
fun MutableLiveData.setState(reducer: T.() -> T) {
this.value = this.value?.reducer()
}
distinctUntilChanged
来实现防抖LiveData
默认是不防抖的,这样改造后就是防抖的了,所以传入相同的值是不会回调的State
的数据类需要防混淆上面介绍了LiveData
如何实现属性监听,下面看下简单的使用
//页面状态,需要避免混淆
data class MainViewState(
val fetchStatus: FetchStatus = FetchStatus.NotFetched,
val newsList: List = emptyList()
)
//ViewModel
class MainViewModel : ViewModel() {
private val _viewStates: MutableLiveData = 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) {
//..
}
}
}
}
如上所示,其实使用起来也很简单方便
ViewModel
只需对外暴露一个ViewState
,避免了定义多个可变不可变LiveData
的问题View
层支持监听LiveData
的一个属性或多个属性,支持局部刷新本文主要介绍了MVI
架构下如何实现局部刷新,并重点介绍了Mavericks
的基本使用与原理,并在其基础上使用LiveData
实现了属性监听与局部刷新
通过以上方式,解决了MVI
架构的性能问题,实现了MVI
架构的更佳实践
如果你的ViewModel
中定义了多个可变与不可变的LiveData
,就算你不使用MVI
架构,支持监听LiveData
属性相信也可以帮助你精简一定的代码
如果本文对你有所帮助,欢迎点赞关注~