前言
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<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}"
}
}
如上所示,看上去也很简单,主要包括几个模块
- 包括页面所有状态的
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 <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)
}
- 主要是通过
map
将State
转化为它的属性值 - 通过
distinctUntilChanged
方法开启防抖,相同的值不会回调,只有值修改了才会回调 - 需要注意的是因为使用了
KProperty1
,因此State
的承载数据类必须避免混淆
如上,就是Mavericks
的基本介绍,想了解更多的同学可参考:github.com/airbnb/mave…
LiveData
实现属性监听
上面介绍了Mavericks
是怎么实现局部刷新的,但直接使用它主要有两个问题
- 接入起来略微有点麻烦,例如
Fragment
必须实现MavericksView
,有一定接入成本 -
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()
}
- 如上所示,主要是添加一个扩展方法,也是通过
distinctUntilChanged
来实现防抖 - 如果需要监听多个属性,例如两个属性有其中一个变化了就触发刷新,也支持传入两个属性
- 需要注意的是
LiveData
默认是不防抖的,这样改造后就是防抖的了,所以传入相同的值是不会回调的 - 同时需要注意下承载
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) {
//..
}
}
}
}
如上所示,其实使用起来也很简单方便
-
ViewModel
只需对外暴露一个ViewState
,避免了定义多个可变不可变LiveData
的问题 -
View
层支持监听LiveData
的一个属性或多个属性,支持局部刷新
总结
本文主要介绍了MVI
架构下如何实现局部刷新,并重点介绍了Mavericks
的基本使用与原理,并在其基础上使用LiveData
实现了属性监听与局部刷新
通过以上方式,解决了MVI
架构的性能问题,实现了MVI
架构的更佳实践
如果你的ViewModel
中定义了多个可变与不可变的LiveData
,就算你不使用MVI
架构,支持监听LiveData
属性相信也可以帮助你精简一定的代码
如果本文对你有所帮助,欢迎点赞关注Star
~