Android进阶宝典 您所在的位置:网站首页 安卓架构设计总结 Android进阶宝典

Android进阶宝典

2023-08-29 01:20| 来源: 网络整理| 查看: 265

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



【本文地址】

公司简介

联系我们

今日新闻

    推荐新闻

      专题文章
        CopyRight 2018-2019 实验室设备网 版权所有