Google官方架构MVI
如果经常看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 //页面loadingval 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) {//显示loadingbinding.csLoading.visibility = View.VISIBLE} else if (!state.isLoading && state.weatherData != null) {//展示数据binding.csLoading.visibility = View.GONEbinding.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 //页面loadingval isError = false //页面错误val weatherData:WeatherRealTime? = null //实时天气数据
}
那么这样设计有什么好处呢?因为状态不可变,在UI层就无法改变这个状态的值,因为在UI层改变状态可能会影响到其他订阅者的状态,而且UI层本来就是禁止改变状态的,除非当前页面是数据的唯一来源,例如:
binding.btnGet.setOnClickListener { canSubmit = trueif(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.GONEbinding.tvTemperature.text = state.weatherData.temperature
} else if (!state.isLoading && state.isError) {//展示错误页面
}
使用属性扩展即可
//是否有数据,正常状态下
val WeatherUiState.hasData: Booleanget() = !isLoading && weatherData != null
//发生错误
val WeatherUiState.error: Booleanget() = !isLoading && isError
简化后的UI层处理逻辑:
lifecycleScope.launchWhenCreated {viewModel.weatherUiState.collectLatest { state ->Log.e("TAG", "state ==> $state")if (state.isLoading) {//显示loadingbinding.csLoading.visibility = View.VISIBLE} else if (state.hasData) {//展示数据binding.csLoading.visibility = View.GONEbinding.tvTemperature.text = state.weatherData?.temperature} else if (state.error) {//展示错误页面}}
}
综上所述,大家可能对于单向数据流这种模式有一些了解,而且为何使用单向数据流,官方也有自己的说法
- 数据一致性: 界面只有一个可信来源。
- 可测试性: 状态来源是独立的,因此可独立于界面进行测试。
- 可维护性: 状态的更改遵循明确定义的模式,即状态更改是用户事件及其数据拉取来源共同作用的结果。
数据唯一性,因为对于MVI架构来说,数据就是UiState,每个页面监听这个UiState,而且只来源于ViewModel且不可变,不能通过UI层改变其状态;如果发生了改变,只能是ViewModel推动状态的改变,所以数据流是单向的,这才是真正的数据驱动UI;
而且可以追本溯源,某个状态出现问题,就可以直接定位到状态更新的位置,查明问题的原因。
当然这也是Google最近才推出来的架构模式,目前主流的依然还是MVVM
其实还有许多Android 架构方面知识点,想跟大家分享,但由于文章篇幅长度限制,我已将其整理成了学习手册的形式,供大家进行参考学习,有需要的可以 点击直接看↓↓↓方 进行参考学习
Google官方架构MVI相关推荐
- google官方mvp+dagger2架构详解
原文链接:http://www.jianshu.com/p/01d3c014b0b1 1 前言 前段时间分享了一篇文章:google官方架构MVP解析与实战 ,针对这是对google官方示例架构的一个 ...
- Android官方架构组件Paging:分页库的设计美学
本文已授权 微信公众号 玉刚说 (@任玉刚)独家发布. 2019/12/24 补充 距本文发布时隔一年,笔者认为,本文不应该作为入门教程的第一篇博客,相反,读者真正想要理解 Paging 的使用,应该 ...
- Sunflower——Google官方的Jetpack学习项目笔记(Java版)
由于Google官网给出的该项目是Kotlin版本,我将其改造成Java版本,供大家学习参考,文末给出下载链接,里面包含了详细的注释说明. 该项目虽然简单,但是用到的知识很多,正所谓麻雀虽小五脏俱全, ...
- Android 官方架构组件 Navigation 使用详解
前言 前段时间,我在做项目开发的时候对Fragment的管理遇到几个小问题,总觉得在现阶段封装好的Fragment管理器不太优雅.这成为我下决心学习Jetpack在很早之前推出的Navigation库 ...
- Android的非Google官方衍生品
Android的非Google官方衍生品 什么是Android的Google官方衍生品 Android Wear Android Auto Android TV Android的非Google官方衍生 ...
- android google 下拉刷新 csdn,android SwipeRefreshLayout google官方下拉刷新控件
下拉刷新功能之前一直使用的是XlistView很方便我前面的博客有介绍 SwipeRefreshLayout是google官方推出的下拉刷新控件使用方法也比较简单 今天就来使用下SwipeRefres ...
- 【Android 应用开发】Google 官方 EasyPermissions 权限申请库 ( 最简单用法 | 一行代码搞定权限申请 | 推荐用法 )
文章目录 一.添加依赖 二.在 AndroidManifest.xml 中配置权限 三.权限申请最简单用法 四.推荐使用的用法 五.GitHub 地址 上一篇博客 [Android 应用开发]Goog ...
- 【Android 内存优化】Bitmap 硬盘缓存 ( Google 官方 Bitmap 示例 | DiskLruCache 开源库 | 代码示例 )
文章目录 一.Google 官方 Bitmap 相关示例参考 二.磁盘缓存类 DiskLruCache 三.磁盘缓存初始化 四.存储数据到磁盘缓存中 五.从磁盘缓存中读取数据 六. Android 1 ...
- Google MapReduce架构设计
前情回顾 Google MapReduce到底解决什么问题? Google MapReduce是Google产出的一个编程模型,同时Google也给出架构实现,它能够解决"能用分治法解决的问 ...
- Android菜鸟的成长笔记(28)——Google官方对Andoird 2.x提供的ActionBar支持
在Google官方Android设计指南中(链接:http://www.apkbus.com/design/get-started/ui-overview.html)有一个新特性就是自我标识,也就是宣 ...
最新文章
- 卡写入速度_看清商家买相机送SD卡的套路,一文教你掌握存储卡选购秘诀
- 学习笔记——基本光照模型简单实现
- vaadin_Vaadin Flow –奇妙的鹿
- 2021 年 Linux 界的 12 件大事
- LeetCode 274. H-Index
- Linux性能基础:CPU、内存、磁盘等概述
- 菲仕乐高压锅型号全面详解
- matlab画平面风羽图(彩色)
- VS Visual Studio 2022调试控制台 输出不全 不完整 缺内容 少了很多代码 有屋设计拆单管理一体化软件 全屋定制拆单 橱柜衣柜整装 木门归方程序
- Java未来城市练习代码01
- 史上最全BigDecimal的5种进位方式:ROUND_UP,ROUND_DOWN,ROUND_CEILING,ROUND_FLOOR,ROUND_HALF_UP,ROUND_HALF_DOWN的比较
- 强化学习基础05——gym
- 在家参加OCP考试(MySQL OCP和Oracle OCP)
- 前程无忧推进私有化:CEO甄荣辉持股19%,多次陷入信息泄露风波
- 2.PRT文件的解析
- Java七大排序(详细总结)
- WPS文字文档下面有红色波浪线怎么去除
- 2021-07-20
- cas服务端配置oracle,CAS搭建单点登陆服务端配置
- python中对列表排序_在Python中对嵌套列表进行排序和分组