如果经常看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相关推荐

  1. google官方mvp+dagger2架构详解

    原文链接:http://www.jianshu.com/p/01d3c014b0b1 1 前言 前段时间分享了一篇文章:google官方架构MVP解析与实战 ,针对这是对google官方示例架构的一个 ...

  2. Android官方架构组件Paging:分页库的设计美学

    本文已授权 微信公众号 玉刚说 (@任玉刚)独家发布. 2019/12/24 补充 距本文发布时隔一年,笔者认为,本文不应该作为入门教程的第一篇博客,相反,读者真正想要理解 Paging 的使用,应该 ...

  3. Sunflower——Google官方的Jetpack学习项目笔记(Java版)

    由于Google官网给出的该项目是Kotlin版本,我将其改造成Java版本,供大家学习参考,文末给出下载链接,里面包含了详细的注释说明. 该项目虽然简单,但是用到的知识很多,正所谓麻雀虽小五脏俱全, ...

  4. Android 官方架构组件 Navigation 使用详解

    前言 前段时间,我在做项目开发的时候对Fragment的管理遇到几个小问题,总觉得在现阶段封装好的Fragment管理器不太优雅.这成为我下决心学习Jetpack在很早之前推出的Navigation库 ...

  5. Android的非Google官方衍生品

    Android的非Google官方衍生品 什么是Android的Google官方衍生品 Android Wear Android Auto Android TV Android的非Google官方衍生 ...

  6. android google 下拉刷新 csdn,android SwipeRefreshLayout google官方下拉刷新控件

    下拉刷新功能之前一直使用的是XlistView很方便我前面的博客有介绍 SwipeRefreshLayout是google官方推出的下拉刷新控件使用方法也比较简单 今天就来使用下SwipeRefres ...

  7. 【Android 应用开发】Google 官方 EasyPermissions 权限申请库 ( 最简单用法 | 一行代码搞定权限申请 | 推荐用法 )

    文章目录 一.添加依赖 二.在 AndroidManifest.xml 中配置权限 三.权限申请最简单用法 四.推荐使用的用法 五.GitHub 地址 上一篇博客 [Android 应用开发]Goog ...

  8. 【Android 内存优化】Bitmap 硬盘缓存 ( Google 官方 Bitmap 示例 | DiskLruCache 开源库 | 代码示例 )

    文章目录 一.Google 官方 Bitmap 相关示例参考 二.磁盘缓存类 DiskLruCache 三.磁盘缓存初始化 四.存储数据到磁盘缓存中 五.从磁盘缓存中读取数据 六. Android 1 ...

  9. Google MapReduce架构设计

    前情回顾 Google MapReduce到底解决什么问题? Google MapReduce是Google产出的一个编程模型,同时Google也给出架构实现,它能够解决"能用分治法解决的问 ...

  10. Android菜鸟的成长笔记(28)——Google官方对Andoird 2.x提供的ActionBar支持

    在Google官方Android设计指南中(链接:http://www.apkbus.com/design/get-started/ui-overview.html)有一个新特性就是自我标识,也就是宣 ...

最新文章

  1. 卡写入速度_看清商家买相机送SD卡的套路,一文教你掌握存储卡选购秘诀
  2. 学习笔记——基本光照模型简单实现
  3. vaadin_Vaadin Flow –奇妙的鹿
  4. 2021 年 Linux 界的 12 件大事
  5. LeetCode 274. H-Index
  6. Linux性能基础:CPU、内存、磁盘等概述
  7. 菲仕乐高压锅型号全面详解
  8. matlab画平面风羽图(彩色)
  9. VS Visual Studio 2022调试控制台 输出不全 不完整 缺内容 少了很多代码 有屋设计拆单管理一体化软件 全屋定制拆单 橱柜衣柜整装 木门归方程序
  10. Java未来城市练习代码01
  11. 史上最全BigDecimal的5种进位方式:ROUND_UP,ROUND_DOWN,ROUND_CEILING,ROUND_FLOOR,ROUND_HALF_UP,ROUND_HALF_DOWN的比较
  12. 强化学习基础05——gym
  13. 在家参加OCP考试(MySQL OCP和Oracle OCP)
  14. 前程无忧推进私有化:CEO甄荣辉持股19%,多次陷入信息泄露风波
  15. 2.PRT文件的解析
  16. Java七大排序(详细总结)
  17. WPS文字文档下面有红色波浪线怎么去除
  18. 2021-07-20
  19. cas服务端配置oracle,CAS搭建单点登陆服务端配置
  20. python中对列表排序_在Python中对嵌套列表进行排序和分组

热门文章

  1. 教你利用腾讯云cdn加速网站静态资源
  2. 凤凰院凶真 解题报告
  3. php实现把二叉树打印成多行(谋而后动,写好算法思路,不然浪费超多时间而且还是错误代码,而且精力消耗会导致代码正确率下降以及低级错误)...
  4. 因特尔显卡自定义分辨率_Win10创建自定义分辨率的方法教程
  5. Foundation框架之字符串和日期
  6. JavaWeb项目启动时,自动执行指定方法
  7. eclipse下载与安装(汉化教程)超详细
  8. 计算机网络常用五种编码方式,五种常见的基带数字编码方式
  9. 大数据与人工智能方向基础课程简单介绍
  10. Java web前端——HTML常用标签