Android进阶宝典 -- Google官方架构MVI

5,447 阅读6分钟

如果经常看Google官方文档的伙伴,可能早就发现,Google官方应用架构指南中推荐的架构模式已经不是MVVM,而是一种全新的MVI架构,先把官方的架构图贴出来

image.png

我们可以看到常见的数据层和UI层还是存在的,中间则是穿插了一个用于做数据层和UI层通信的架构层,类似于MVVM中ViewModel的角色类型,UI层依赖中间层,中间层依赖数据层。

1 MVI架构的优势

既然Google推出这个架构,那么这个架构必然是存在自身的优势,MVVM已经是大众常见的架构模式,那么MVI相较于MVVM做了什么升级呢?

首先我们回顾下MVVM的架构,如下图所示

image.png

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的伙伴应该不陌生,单向数据流可以认为是一种设计模式,状态自上而下,事件自下而上;

image.png 而且UI层更改状态不会影响数据源的数据,这种优势在于数据来源是唯一的,针对状态可以定位问题

2 MVI架构设计

从第一小节中,我们大概知道了MVI的几个显著特点,现在我们通过代码,来一步一步实现一个简单的MVI架构应用,这里用聚合数据中的一个接口:查询天气预报 apis.juhe.cn/simpleWeath…

2.1 界面层

因为MVI的一个特点就是UI状态集中管理,因此UI层除了UI Element之外,还需要一个UiState类将所有的状态集中管理。

image.png

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,如果有想尝试这个架构设计模式(我已经在项目中开始使用了),伙伴们可以一起来讨论