随着Android应用不断的演化,从最开始的MVC->MVP->MVVM,现在Google官方也有了MVI的示例,相比较MVVM来说有了一些变化,接下来跟着文章一起了解.(其实这篇文章主要是介绍MVI的,和Compose UI没啥关系,但是因为后面一篇Compose UI + MVI),需要有前置了解MVI会更容易理解,所以有第二篇常规的MVI了解.
其实MVI和MVVM是类似的,因为MVI是在MVVM的基础上产生的演变.MVVM中我们会通过定义函数方法执行对应的点击逻辑,或通过databingding,或通过再Activity/Fragment中设置事件来触发,但是MVI中对于数据的流向变成了单向的,即MVVM中对LiveData是没有做限制的,因为适配了很多业务需要有很多数据LiveData来承载.MVI中事件通过Intent传入ViewModle后触发逻辑操作,再通过订阅反馈到界面,但是这里的一个订阅数据往往管理着多个状态,这样更有利于对数据状态的管理.
class MVIViewModel : ViewModel() {
val mainIntentChannel = Channel<MVI3Intent>(Channel.UNLIMITED)
private val _viewStates: MutableLiveData<Demo14ViewState> = MutableLiveData(Demo14ViewState())
//只需要暴露一个LiveData,包括页面所有状态
val viewStates: LiveData<Demo14ViewState> = _viewStates
init {
viewModelScope.launch {
mainIntentChannel.consumeAsFlow().collect { value ->
//接收到不同的意图后,去触发不同的逻辑,例如请求数据,保存数据等
}
}
}
}
data class Demo14ViewState(
val industrys: ArrayList<Industry> = arrayListOf(),
val schools: ArrayList<SchoolBean> = arrayListOf(),
var isChanged: Boolean = false
) : BaseViewState()
open class BaseViewState() {
}
- 以上代码中
mainIntentChannel
作为Intent的意图收集通道,ViewModel
接收到之后就可以去触发对应的逻辑操作,例如向云端请求数据之后反馈给View层,或者是保存数据之类的操作.LiveData
中存储的参数不再是一个Bean
类或者是某一个集合数据,而且一个组合的UI
状态集,其中包含了界面上面多个状态的数据集以及逻辑Flag
- 在组合数据对象定义的时候就可以根据自己的业务去扩展需要的数据集,这里会产生一些疑问吧,都在一个对象中,怎么去更新数据呢? 一直操作这一个对象? 这里能够首先感受到 , 数据都维护到这个
State
对象中去了,等于后续需要什么都是从这个状态中获取,有了集中管理,看起来没有这么散乱了.- 这里把暴露出去的对象和
ViewModel
内部使用的Livedata做了区分,_viewState
和viewState
两个变量,viewState
指定的类型是LiveData
,这样外部就不可以再改变数据了,把数据逻辑处理都限制在了ViewModel
内部- 最后我们再
init
块中,通过订阅接收View
层传递的意图
页面中我们获取ViewModel还是和以前一样的,只不过在订阅数据和点击事件有了变化
class MVIActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.act_mvi)
bTest.setOnClickListener {
lifecycleScope.launch {
viewModel.mainIntentChannel.send(MVI3Intent.UpdateChange(false))
}
}
}
}
sealed class MVI3Intent {
//行为- 想要获取行业数据
object GetIndustry : MVI3Intent()
//行为- 想要获取学校数据
sealed class GetSchool() : MVI3Intent()
data class UpdateChange(var isChange: Boolean) : MVI3Intent()
}
点击事件中我们不再调用具体的函数,直接执行,而是通过刚才在
ViewModel
中定义的Channel
去发送意图,这种定义在Intent中定义多种类型,对应不同的业务.(PS:这里的Intent
和Acitvity
的Intent
不一样,不过也类似,因为也可以携带很多参数啥的)
override fun onCreate(savedInstanceState: Bundle?) {
viewModel.viewStates.observeState(this, Demo14ViewState::industrys) { industrys ->
//触发了内容改变
}
}
fun <T, A> LiveData<T>.observeState(
lifecycleOwner: LifecycleOwner, prop1: KProperty1<T, A>, action: (A) -> Unit
) {
this.map {
WarpBean(prop1.get(it))
}.distinctUntilChanged().observe(lifecycleOwner) { a ->
action.invoke(a.content1)
}
}
data class WarpBean<T>(var content1: T)
在页面中我们再订阅数据的时候,可以通过
obserState
函数去订阅,这里我们做了扩展函数的封装,可以针对性的对想要的某一个数据进行订阅,比如实际业务中,数据集合含有滚动栏目,推荐栏目,商品栏目,小程序栏目,但是某个业务只依赖小程序的数据,就可以只订阅对象中的某一个数据,通过代理的形式.
override fun onCreate(savedInstanceState: Bundle?) {
viewModel.viewStates.observeState(this, Demo14ViewState::industrys, Demo14ViewState::schools) { industrys, schools ->
}
}
fun <T, A, B> LiveData<T>.observeState(
lifecycleOwner: LifecycleOwner, prop1: KProperty1<T, A>, prop2: KProperty1<T, B>, action: (A, B) -> Unit
) {
this.map {
WarpBean2(prop1.get(it), prop2.get(it))
}.distinctUntilChanged().observe(lifecycleOwner) { a ->
action.invoke(a.content1, a.content2)
}
}
data class WarpBean2<T, E>(var content1: T, var content2: E)
可以通过两个代理去实现订阅两个数据条目的方式,关注1个数据源还是2个数据源还是多个,还是整个,可以动态的根据业务的需求,灵活使用.
在文章最开始在ViewModle
中通过了LiveData
去分发UI数据,但是LiveData
会在页面重建旋转等场景下重新触发订阅,但是Toast
提示和Dialog
这种临时的弹窗是不需要再切换白天黑夜,旋转等重建场景下再次提示的,所以这里我们引入Flow
管理,包括文章一开头的LiveData
也可以通过Flow
来实现
private val _uiStates = MutableStateFlow<Demo14ViewState>(Demo14ViewState())
val uiStates: StateFlow<Demo14ViewState> get() = _uiStates
private val _uiEffect = MutableSharedFlow<UiEffect>()
val uiEffect by lazy { _uiEffect.asSharedFlow() }
和文章一开始是一样的,对外暴露的是
StateFlow
以及ShardeFlow
,但是ViewModel
内部使用的时候还是使用的MutableStateFlow
和MutableSharedFlow
,这里的uiStates
就是用来反馈持久化的UI数据状态的,会在切换白天黑夜和屏幕旋转之类的场景重建再次订阅的时候接收到数据,uiEffect
就是用来单次通知,例如Toast
提示和弹窗提示等操作的时候,不会在重建的场景下订阅再次触发.
在MVI
中,ViewModel
不再有很多的被View
层调用的函数方法,因为都通过Channel
来统一接收了,也不再有很多的LiveData
了,通过了数据的组合,再通过拆分数据订阅来实现业务,数据也统一管理了,这对封装来说有了更大的空间,可以限制子类的业务代码.但是实际业务使用还是要根据具体的业务来选择合适的架构.MVI
有独有的优势,但是对应的数据管理相应的类也会增多.架构带来的层级越复杂,层数越多,可以扩展的地方也越多,但是后续迭代升级带来的复杂度也会越高,比如新加入开发人员上手难度会更大,但是如果业务的复杂度上来了,确实需要那完全是可以选择的.