如果经常看Google官方文档的伙伴,可能早就发现,Google官方应用架构指南中推荐的架构模式已经不是MVVM,而是一种全新的MVI架构,先把官方的架构图贴出来
我们可以看到常见的数据层和UI层还是存在的,中间则是穿插了一个用于做数据层和UI层通信的架构层,类似于MVVM中ViewModel的角色类型,UI层依赖中间层,中间层依赖数据层。
1 MVI架构的优势
既然Google推出这个架构,那么这个架构必然是存在自身的优势,MVVM已经是大众常见的架构模式,那么MVI相较于MVVM做了什么升级呢?
首先我们回顾下MVVM的架构,如下图所示
VM层与数据层单向绑定,从数据层获取数据;UI层和VM层做数据的双向绑定,通过ViewModel层数据变化驱动UI层更新。
所以MVVM架构是UI层持有VM层的数据做监听,并刷新UI数据,而MVI呢?我个人认为它和MVVM是非常像的,与MVVM不同的是,MVI是做UI状态的集中管理,并以单向数据流的形式,将UI的状态输出到UI层,UI层根据状态做相应的处理。
这里提到了MVI架构的2个特点:
(1)UI状态集中管理;
(2)单向数据流;
在MVVM架构中,并没有UI状态这个概念,而是UI层根据数据的变化,做页面状态判断并展示,当然也可以在VM层做状态管理,但更多的是一个state对应一个LiveData,无法做到集中管理;
第二就是单向数据流,如果做过前端或者IOS的伙伴应该不陌生,单向数据流可以认为是一种设计模式,状态自上而下,事件自下而上;
而且UI层更改状态不会影响数据源的数据,这种优势在于数据来源是唯一的,针对状态可以定位问题
2 MVI架构设计
从第一小节中,我们大概知道了MVI的几个显著特点,现在我们通过代码,来一步一步实现一个简单的MVI架构应用,这里用聚合数据中的一个接口:查询天气预报 apis.juhe.cn/simpleWeath…
2.1 界面层
因为MVI的一个特点就是UI状态集中管理,因此UI层除了UI Element之外,还需要一个UiState类将所有的状态集中管理。
class WeatherUiState {
val isLoading = false //页面loading
val isError = false //页面错误
val weatherData:WeatherRealTime? = null //实时天气数据
}
在WeatherUiState中,定义了页面的3种状态,分别是数据在加载过程中的Loading状态、加载失败的状态error,请求到数据之后展示的页面数据;
在MVVM架构中,我们经常在UI层监听ViewModel数据变化,并在UI处理数据实现业务逻辑,那么在MVI架构中,这种行为是被禁止的,业务逻辑将会放在中间层或者数据层中处理;
那么在MVI架构中,UI层主要处理界面行为逻辑(即界面逻辑)决定着如何在屏幕上显示状态变化。例如使用 Android Resources获取要在屏幕上显示的正确文本、在用户点击某个按钮时转到特定屏幕,或者使用Toast弹出提示等
那么在ViewModel中,需要暴露这个状态让UI层去获取,例如:
class WeatherVM {
private val _weatherUiState: MutableStateFlow<WeatherUiState> = MutableStateFlow(WeatherUiState())
val weatherUiState: StateFlow<WeatherUiState> = _weatherUiState.asStateFlow()
}
使用MutableStateFlow封装WeatherUiState,这里为什么不用LiveData,稍后再说。
这里我们想一个问题就是,我们现在是把所有的状态全部封装到一起,在ViewModel中只存在单一的数据流,那么是否需要限制一定使用单一数据流?
其实不是的,关键需要看状态之间的关联性,例如当页面加载完成之后,有两种情况:
1 获取到数据显示数据
2 接口数据获取失败,网络异常 or 服务器异常\
这种状态其实是强关联的,封装在一起是没有问题;但是如果存在一种状态与上述的状态不存在关联状态,那么就可以将这个状态单独封装成一个状态类,作为另一个数据流存储在ViewModel中。
2.2 Intent层
这里就是所谓的I层,试图事件层,用于接受UI层的事件触发,向数据层获取数据。
binding.btnGet.setOnClickListener {
viewModel.getWeather()
}
当用户触发获取天气的意图的时候,请求ViewModel中的一个方法,那么在这个方法中,就会进行状态的分发,当发起请求之前,会有loading页面,然后请求结束之后,loading动画消失; 会判断获取到的数据是否正常,如果不为空,那么就将数据回调出去;如果数据出现异常,那么就将错误页面的回调给UI层
fun getWeather() {
viewModelScope.launch {
_weatherUiState.update {
it.copy(isLoading = true)
}
val result = WeatherDataSource.getWeather("北京")
_weatherUiState.update {
it.copy(isLoading = false)
}
if (result.result?.realtime != null) {
_weatherUiState.update {
it.copy(weatherData = result.result.realtime)
}
} else {
//异常
_weatherUiState.update {
it.copy(isError = true)
}
}
}
}
如此一来,UI层的主要作用就是处理这些状态的回调并展示数据,例如:
lifecycleScope.launchWhenCreated {
viewModel.weatherUiState.collectLatest { state ->
Log.e("TAG", "state ==> $state")
if (state.isLoading) {
//显示loading
binding.csLoading.visibility = View.VISIBLE
} else if (!state.isLoading && state.weatherData != null) {
//展示数据
binding.csLoading.visibility = View.GONE
binding.tvTemperature.text = state.weatherData.temperature
} else if (!state.isLoading && state.isError) {
//展示错误页面
}
}
}
对于数据层这里就不再赘述了,这部分跟MVP、MVVM其实是一致的。
前面我们提到过,为什么不去使用LiveData,而是采用StateFlow,那么我们使用LiveData看一下效果,会不会有什么问题
fun getWeatherByLiveData() {
viewModelScope.launch {
weatherLiveData.postValue(WeatherUiState(isLoading = true))
val result = WeatherDataSource.getWeather("北京")
weatherLiveData.postValue(WeatherUiState(isLoading = false))
if (result.result?.realtime != null) {
weatherLiveData.postValue(
WeatherUiState(
isLoading = false,
weatherData = result.result.realtime
)
)
} else {
//异常
weatherLiveData.postValue(WeatherUiState(isLoading = false, isError = true))
}
}
}
我们这里依然回调了3次状态,但是UI层只收到了2次状态的回调,也就是说因为LiveData的特性(回调最新的数据),可能会有部分状态数据丢失的问题,但是如果使用Flow就不会存在这个问题,因为数据流是不会断层的。
2022-10-02 21:12:09.162 6356-6356/com.lay.mvi E/TAG: state ==> WeatherUiState(isLoading=true, isError=false, weatherData=null)
2022-10-02 21:12:09.525 6356-6356/com.lay.mvi E/TAG: state ==> WeatherUiState(isLoading=false, isError=false, weatherData=WeatherRealTime(temperature=19, humidity=89, info=阴, wid=02, direct=东风, power=2级, aqi=15))
3 UiState总结
3.1 不可变性
我们可以发现,在定义页面状态的时候,每个属性值就是不可变的,也就是说整个状态是不可变的。
class WeatherUiState {
val isLoading = false //页面loading
val isError = false //页面错误
val weatherData:WeatherRealTime? = null //实时天气数据
}
那么这样设计有什么好处呢?因为状态不可变,在UI层就无法改变这个状态的值,因为在UI层改变状态可能会影响到其他订阅者的状态,而且UI层本来就是禁止改变状态的,除非当前页面是数据的唯一来源,例如:
binding.btnGet.setOnClickListener {
canSubmit = true
if(canSubmit){
it.background = resources.getDrawable(R.drawable.ic_launcher_background)
}
}
这种属于界面行为逻辑,而不是业务逻辑,这种是可以在UI层做状态的变化
还有一个优势在于:UiState始终会存储当前页面的最新状态,即便页面配置发生改变之后,UiState依然是不变的,这也是跟ViewModel存储特性结合起来了。
2022-10-02 22:01:46.901 6918-6918/com.lay.mvi E/TAG: state ==> WeatherUiState(isLoading=false, isError=false, weatherData=null)
2022-10-02 22:01:49.667 6918-6918/com.lay.mvi E/TAG: state ==> WeatherUiState(isLoading=true, isError=false, weatherData=null)
2022-10-02 22:01:50.096 6918-6918/com.lay.mvi E/TAG: state ==> WeatherUiState(isLoading=false, isError=false, weatherData=null)
2022-10-02 22:01:50.097 6918-6918/com.lay.mvi E/TAG: state ==> WeatherUiState(isLoading=false, isError=false, weatherData=WeatherRealTime(temperature=18, humidity=91, info=阴, wid=02, direct=东北风, power=2级, aqi=15))
3.2 UiState扩展
在上一小节中,我们看到UI层在监听状态变化时,会结合多个状态来判断应该展示哪个页面,这种其实完全没有必要,因为真正要做到UI层只做页面展示,这种判断就可以直接放在UiState中处理即可
else if (!state.isLoading && state.weatherData != null) {
//展示数据
binding.csLoading.visibility = View.GONE
binding.tvTemperature.text = state.weatherData.temperature
} else if (!state.isLoading && state.isError) {
//展示错误页面
}
使用属性扩展即可
//是否有数据,正常状态下
val WeatherUiState.hasData: Boolean
get() = !isLoading && weatherData != null
//发生错误
val WeatherUiState.error: Boolean
get() = !isLoading && isError
简化后的UI层处理逻辑:
lifecycleScope.launchWhenCreated {
viewModel.weatherUiState.collectLatest { state ->
Log.e("TAG", "state ==> $state")
if (state.isLoading) {
//显示loading
binding.csLoading.visibility = View.VISIBLE
} else if (state.hasData) {
//展示数据
binding.csLoading.visibility = View.GONE
binding.tvTemperature.text = state.weatherData?.temperature
} else if (state.error) {
//展示错误页面
}
}
}
综上所述,大家可能对于单向数据流这种模式有一些了解,而且为何使用单向数据流,官方也有自己的说法
- 数据一致性: 界面只有一个可信来源。
- 可测试性: 状态来源是独立的,因此可独立于界面进行测试。
- 可维护性: 状态的更改遵循明确定义的模式,即状态更改是用户事件及其数据拉取来源共同作用的结果。
数据唯一性,因为对于MVI架构来说,数据就是UiState,每个页面监听这个UiState,而且只来源于ViewModel且不可变,不能通过UI层改变其状态;如果发生了改变,只能是ViewModel推动状态的改变,所以数据流是单向的,这才是真正的数据驱动UI;
而且可以追本溯源,某个状态出现问题,就可以直接定位到状态更新的位置,查明问题的原因。
当然这也是Google最近才推出来的架构模式,目前主流的依然还是MVVM,如果有想尝试这个架构设计模式(我已经在项目中开始使用了),伙伴们可以一起来讨论