当前位置: 首页 > news >正文

MVVM 架构进阶:MVI 架构详解

前言

Android开发发展到今天已经相当成熟了,各种架构大家也都耳熟能详,如MVC,MVP,MVVM等,其中MVVM更是被官方推荐,成为Android开发中的显学。

不过软件开发中没有银弹,MVVM架构也不是尽善尽美的,在使用过程中也会有一些不太方便之处,而MVI可以很好的解决一部分MVVM的痛点。

本文主要包括以下内容:

1. MVC,MVP,MVVM等经典架构介绍。

2. MVI架构到底是什么?

3. MVI架构实战。

需要重点指出的是,标题中说MVI架构是MVVM的进阶版是指MVI在MVVM非常相似,并在其基础上做了一定的改良,并不是说MVI架构一定比MVVM适合你的项目。

各位同学可以在分析比较各个架构后,选择合适项目场景的架构。

经典架构介绍

MVC架构介绍

MVC是个古老的Android开发架构,随着MVP与MVVM的流行已经逐渐退出历史舞台,我们在这里做一个简单的介绍,其架构图如下所示:

MVC架构主要分为以下几部分:

1.视图层(View):对应于xml布局文件和java代码动态view部分。

2.控制层(Controller):主要负责业务逻辑,在android中由Activity承担,同时因为XML视图功能太弱,所以Activity既要负责视图的显示又要加入控制逻辑,承担的功能过多。

3.模型层(Model):主要负责网络请求,数据库处理,I/O的操作,即页面的数据来源。

由于android中xml布局的功能性太弱,Activity实际上负责了View层与Controller层两者的工作,所以在android中mvc更像是这种形式:

因此MVC架构在android平台上的主要存在以下问题:

1. Activity同时负责View与Controller层的工作,违背了单一职责原则。

2. Model层与View层存在耦合,存在互相依赖,违背了最小知识原则。

MVP架构介绍

由于MVC架构在Android平台上的一些缺陷,MVP也就应运而生了,其架构图如下所示:

MVP架构主要分为以下几个部分:

1. View层:对应于Activity与XML,只负责显示UI,只与Presenter层交互,与Model层没有耦合。

2. Presenter层:主要负责处理业务逻辑,通过接口回调View层。

3. Model层:主要负责网络请求,数据库处理等操作,这个没有什么变化。

我们可以看到,MVP解决了MVC的两个问题,即Activity承担了两层职责与View层与Model层耦合的问题。

但MVP架构同样有自己的问题:

1. Presenter层通过接口与View通信,实际上持有了View的引用。

2. 但是随着业务逻辑的增加,一个页面可能会非常复杂,这样就会造成View的接口会很庞大。

MVVM架构介绍

MVVM 模式将 Presenter 改名为 ViewModel,基本上与 MVP 模式完全一致。

唯一的区别是,它采用双向数据绑定(data-binding):View的变动,自动反映在 ViewModel,反之亦然。

MVVM架构图如下所示:

可以看出MVVM与MVP的主要区别在于,你不用去主动去刷新UI了,只要Model数据变了,会自动反映到UI上。换句话说,MVVM更像是自动化的MVP。

MVVM的双向数据绑定主要通过DataBinding实现,不过相信有很多人跟我一样,是不喜欢用DataBinding的,这样架构就变成了下面这样:

1. View观察ViewModle的数据变化并自我更新,这其实是单一数据源而不是双向数据绑定,所以其实MVVM的这一大特性我其实并没有用到。

2. View通过调用ViewModel提供的方法来与ViewMdoel交互。

小结

1. MVC架构的主要问题在于Activity承担了View与Controller两层的职责,同时View层与Model层存在耦合。

2. MVP引入Presenter层解决了MVC架构的两个问题,View只能与Presenter层交互,业务逻辑放在Presenter层。

3. MVP的问题在于随着业务逻辑的增加,View的接口会很庞大,MVVM架构通过双向数据绑定可以解决这个问题。

4. MVVM与MVP的主要区别在于,你不用去主动去刷新UI了,只要Model数据变了,会自动反映到UI上。换句话说,MVVM更像是自动化的MVP。

5. MVVM的双向数据绑定主要通过DataBinding实现,但有很多人(比如我)不喜欢用DataBinding,而是View通过LiveData等观察ViewModle的数据变化并自我更新,这其实是单一数据源而不是双向数据绑定。

MVI架构到底是什么?

MVVM架构有什么不足?

要了解MVI架构,我们首先来了解下MVVM架构有什么不足。

相信使用MVVM架构的同学都有如下经验,为了保证数据流的单向流动,LiveData向外暴露时需要转化成immutable的,这需要添加不少模板代码并且容易遗忘,如下所示:

class TestViewModel : ViewModel() {//为保证对外暴露的LiveData不可变,增加一个状态就要添加两个LiveData变量private val _pageState: MutableLiveData<PageState> = MutableLiveData()val pageState: LiveData<PageState> = _pageStateprivate val _state1: MutableLiveData<String> = MutableLiveData()val state1: LiveData<String> = _state1private val _state2: MutableLiveData<String> = MutableLiveData()val state2: LiveData<String> = _state2//...
}

如上所示,如果页面逻辑比较复杂,ViewModel中将会有许多全局变量的LiveData,并且每个LiveData都必须定义两遍,一个可变的,一个不可变的。这其实就是我通过MVVM架构写比较复杂页面时最难受的点。

其次就是View层通过调用ViewModel层的方法来交互的,View层与ViewModel的交互比较分散,不成体系。

小结一下,在我的使用中,MVVM架构主要有以下不足:

1. 为保证对外暴露的LiveData是不可变的,需要添加不少模板代码并且容易遗忘。

2. View层与ViewModel层的交互比较分散零乱,不成体系。

MVI架构是什么?

MVI 与 MVVM 很相似,其借鉴了前端框架的思想,更加强调数据的单向流动和唯一数据源,架构图如下所示:

其主要分为以下几部分:

1. Model: 与MVVM中的Model不同的是,MVI的Model主要指UI状态(State)。例如页面加载状态、控件位置等都是一种UI状态。

2. View: 与其他MVX中的View一致,可能是一个Activity或者任意UI承载单元。MVI中的View通过订阅Intent的变化实现界面刷新。(注意:这里不是Activity的Intent)

3. Intent: 此Intent不是Activity的Intent,用户的任何操作都被包装成Intent后发送给Model层进行数据请求。

单向数据流

MVI强调数据的单向流动,主要分为以下几步:

1. 用户操作以Intent的形式通知Model。

2. Model基于Intent更新State。

3. View接收到State变化刷新UI。

数据永远在一个环形结构中单向流动,不能反向流动:

上面简单的介绍了下MVI架构,下面我们一起来看下具体是怎么使用MVI架构的。

MVI架构实战

总体架构图

我们使用ViewModel来承载MVI的Model层,总体结构也与MVVM类似,主要区别在于Model与View层交互的部分。

1. Model层承载UI状态,并暴露出ViewState供View订阅,ViewState是个data class,包含所有页面状态。

2. View层通过Action更新ViewState,替代MVVM通过调用ViewModel方法交互的方式。

MVI实例介绍

添加ViewState与ViewEvent

ViewState承载页面的所有状态,ViewEvent则是一次性事件,如Toast等,如下所示:

data class MainViewState(val fetchStatus: FetchStatus, val newsList: List<NewsItem>)  sealed class MainViewEvent {data class ShowSnackbar(val message: String) : MainViewEvent()data class ShowToast(val message: String) : MainViewEvent()
}

1. 我们这里ViewState只定义了两个,一个是请求状态,一个是页面数据。

2. ViewEvent也很简单,一个简单的密封类,显示Toast与Snackbar。

ViewState更新

class MainViewModel : ViewModel() {private val _viewStates: MutableLiveData<MainViewState> = MutableLiveData()val viewStates = _viewStates.asLiveData()private val _viewEvents: SingleLiveEvent<MainViewEvent> = SingleLiveEvent()val viewEvents = _viewEvents.asLiveData()init {emit(MainViewState(fetchStatus = FetchStatus.NotFetched, newsList = emptyList()))}private fun fabClicked() {count++emit(MainViewEvent.ShowToast(message = "Fab clicked count $count"))}private fun emit(state: MainViewState?) {_viewStates.value = state}private fun emit(event: MainViewEvent?) {_viewEvents.value = event}
}

如上所示:

1. 我们只需定义ViewState与ViewEvent两个State,后续增加状态时在data class中添加即可,不需要再写模板代码。

2. ViewEvents是一次性的,通过SingleLiveEvent实现,当然你也可以用Channel当来实现。

3. 当状态更新时,通过emit来更新状态。

View监听ViewState

private fun initViewModel() {viewModel.viewStates.observe(this) {renderViewState(it)}viewModel.viewEvents.observe(this) {renderViewEvent(it)}
}

如上所示,MVI 使用 ViewState 对 State 集中管理,只需要订阅一个 ViewState 便可获取页面的所有状态,相对 MVVM 减少了不少模板代码。

View通过Action更新State

class MainActivity : AppCompatActivity() {private fun initView() {fabStar.setOnClickListener {viewModel.dispatch(MainViewAction.FabClicked)}}
}
class MainViewModel : ViewModel() {fun dispatch(action: MainViewAction) =reduce(viewStates.value, action)private fun reduce(state: MainViewState?, viewAction: MainViewAction) {when (viewAction) {is MainViewAction.NewsItemClicked -> newsItemClicked(viewAction.newsItem)MainViewAction.FabClicked -> fabClicked()MainViewAction.OnSwipeRefresh -> fetchNews(state)MainViewAction.FetchNews -> fetchNews(state)}}
}

如上所示,View通过Action与ViewModel交互,通过 Action 通信,有利于 View 与 ViewModel 之间的进一步解耦,同时所有调用以 Action 的形式汇总到一处,也有利于对行为的集中分析和监控。

总结

本文主要介绍了MVC,MVP,MVVM与MVI架构,目前MVVM是官方推荐的架构,但仍然有以下几个痛点:

1、MVVM与MVP的主要区别在于双向数据绑定,但由于很多人(比如我)并不喜欢使用DataBindg,其实并没有使用MVVM双向绑定的特性,而是单一数据源。

2. 当页面复杂时,需要定义很多State,并且需要定义可变与不可变两种,状态会以双倍的速度膨胀,模板代码较多且容易遗忘。

3. View与ViewModel通过ViewModel暴露的方法交互,比较凌乱难以维护。

而MVI可以比较好的解决以上痛点,它主要有以下优势:

1 强调数据单向流动,很容易对状态变化进行跟踪和回溯。

2 使用ViewState对State集中管理,只需要订阅一个 ViewState 便可获取页面的所有状态,相对 MVVM 减少了不少模板代码。

3. ViewModel通过ViewState与Action通信,通过浏览ViewState 和 Aciton 定义就可以理清 ViewModel 的职责,可以直接拿来作为接口文档使用。

当然MVI也有一些缺点,比如:

1. 所有的操作最终都会转换成State,所以当复杂页面的State容易膨胀。

2. state是不变的,因此每当state需要更新时都要创建新对象替代老对象,这会带来一定内存开销。

软件开发中没有银弹,所有架构都不是完美的,有自己的适用场景,读者可根据自己的需求选择使用。

但通过以上的分析与介绍,我相信使用MVI架构代替没有使用DataBinding的MVVM是一个比较好的选择。

源码地址:

https://github.com/shenzhen2017/android-architecture

相关文章:

MVVM 架构进阶:MVI 架构详解

前言Android开发发展到今天已经相当成熟了&#xff0c;各种架构大家也都耳熟能详&#xff0c;如MVC,MVP,MVVM等&#xff0c;其中MVVM更是被官方推荐&#xff0c;成为Android开发中的显学。不过软件开发中没有银弹&#xff0c;MVVM架构也不是尽善尽美的&#xff0c;在使用过程中…...

有没有必要考PMP证书?

其实针对有没有必要考试吗&#xff0c;这个可以根本不同行业的人来决定的。 1.高等教育项目管理专业科班出身的人员。 在我国本科学历和硕士研究生学历中&#xff0c;项目管理也有开设。不管以后从事的工作是否为项目管理或其他管理&#xff0c;作为本专业的同学&#xff0c;…...

1 机器学习基础

1 机器学习概述 1.1 数据驱动的问题求解 大数据-Big Data 大数据的多面性 1.2 数据分析 机器学习&#xff1a;海量的数据&#xff0c;获取有用的信息 专门研究计算机怎样模拟或实现人类的学习行为&#xff0c;以获取新的知识或技能&#xff0c;重新组织已有的知识结构使之…...

java基础系列(六) sleep()和wait() 区别

一.前言 关于并发编程这块, 线程的一些基础知识我们得搞明白, 本篇文章来说一下这两个方法的区别,对Android中的HandlerThread机制原理可以有更深的理解, HandlerThread源码理解,请查看笔者的这篇博客: HandlerThread源码理解_handlerthread 源码_broadview_java的博客-CSDN博…...

Urho3D序列化

从Serializable派生的类可以通过定义属性将其自动序列化为二进制或XML格式。属性存储到每个类的上下文中。场景加载/保存和网络复制都是通过从Serializable派生Node和Component类来实现的。 支持的属性类型是Variant支持的所有属性类型&#xff0c;不包括指针和自定义值。 属性…...

企业级信息系统开发学习1.3——利用注解配置取代Spring配置文件

文章目录一、利用注解配置类取代Spring配置文件&#xff08;一&#xff09;打开项目&#xff08;二&#xff09;创建新包&#xff08;三&#xff09;拷贝类与接口&#xff08;四&#xff09;创建注解配置类&#xff08;五&#xff09;创建测试类&#xff08;六&#xff09;运行…...

VUE DIFF算法之快速DIFF

VUE DIFF算法系列讲解 VUE 简单DIFF算法 VUE 双端DIFF算法 文章目录VUE DIFF算法系列讲解前言一、快速DIFF的代码实现二、实践练习1练习2总结前言 本节我们来写一下VUE3中新的DIFF算法-快速DIFF&#xff0c;顾名思义&#xff0c;也就是目前最快的DIFF算法&#xff08;在VUE中&…...

一文掌握如何轻松稿定项目风险管理【静说】

风险管理对于每个项目经理和PMO都非常重要&#xff0c;如果管理不当会出现很多问题&#xff0c;咱们以前分享过很多风险管理的内容&#xff1a; 风险无处不在&#xff0c;一旦发生&#xff0c;会对一个或多个项目目标产生积极或消极影响的确定事件或条件。那么接下来介绍下五大…...

操作系统权限提升(十四)之绕过UAC提权-基于白名单AutoElevate绕过UAC提权

系列文章 操作系统权限提升(十二)之绕过UAC提权-Windows UAC概述 操作系统权限提升(十三)之绕过UAC提权-MSF和CS绕过UAC提权 注&#xff1a;阅读本编文章前&#xff0c;请先阅读系列文章&#xff0c;以免造成看不懂的情况&#xff01;&#xff01; 基于白名单AutoElevate绕过…...

ecology9-谷歌浏览器下-pdf.js在渲染时部分发票丢失文字 问题定位及解决

问题 问题描述 &#xff1a; 在谷歌浏览器下&#xff0c;pdf.js在渲染时部分发票丢失文字&#xff1b;360浏览器兼容模式不存在此问题 排查思路&#xff1a;1、对比谷歌浏览器的css样式和360浏览器兼容模式下的样式&#xff0c;没有发现关键差别 2、✔使用Fiddler修改网页js D…...

JavaScript Window Navigator

文章目录JavaScript Window NavigatorWindow Navigator警告!!!浏览器检测JavaScript Window Navigator window.navigator 对象包含有关访问者浏览器的信息。 Window Navigator window.navigator 对象在编写时可不使用 window 这个前缀。 实例 <div id"example"…...

Linux基础命令-du查看文件的大小

文章目录 du 命令介绍 语法格式 基本参数 参考实例 1&#xff09;以人类可读形式显示指定的文件大小 2&#xff09;显示当前目录下所有文件大小 3&#xff09;只显示目录的大小 4&#xff09;显示根下哪个目录文件最大 5&#xff09;显示所有文件的大小 6&#xff0…...

文献计量分析方法:Citespace安装教程

Citespace是一款由陈超美教授开发的可用于海量文献可视化分析的软件&#xff0c;可对Web of Science&#xff0c;Scopus&#xff0c;Pubmed&#xff0c;CNKI等数据库的海量文献进行主题、关键词&#xff0c;作者单位、合作网络&#xff0c;期刊、发表时间&#xff0c;文献被引等…...

MVI 架构更佳实践:支持 LiveData 属性监听

前言MVI架构为了解决MVVM在逻辑复杂时需要写多个LiveData(可变不可变)的问题,使用ViewState对State集中管理&#xff0c;只需要订阅一个 ViewState 便可获取页面的所有状态通过集中管理ViewState&#xff0c;只需对外暴露一个LiveData&#xff0c;解决了MVVM模式下LiveData膨胀…...

LeetCode438 找到字符串中所有字母异位词 带输入和输出

题目&#xff1a; 给定两个字符串 s 和 p&#xff0c;找到 s 中所有 p 的 异位词 的子串&#xff0c;返回这些子串的起始索引。不考虑答案输出的顺序。 异位词 指由相同字母重排列形成的字符串&#xff08;包括相同的字符串&#xff09;。 示例 1: 输入: s “cbaebabacd”, …...

ACSC 2023 比赛复现

Admin Dashboard 在 index.php 中可以看到需要访问者是 admin 权限&#xff0c;才可以看到 flag。 report.php 中可以让 admin bot 访问我们输入的 url&#xff0c;那么也就是说可以访问 addadmin.php 添加用户。 在 addadmin.php 中可以添加 admin 用户&#xff0c;但是需…...

【Linux驱动开发100问】什么是模块?如何编写和使用模块?

&#x1f947;今日学习目标&#xff1a;什么是Linux内核&#xff1f; &#x1f935;‍♂️ 创作者&#xff1a;JamesBin ⏰预计时间&#xff1a;10分钟 &#x1f389;个人主页&#xff1a;嵌入式悦翔园个人主页 &#x1f341;专栏介绍&#xff1a;Linux驱动开发100问 什么是模块…...

Android 9.0 Recent列表不显示某个app

1.概述 在9.0的系统产品rom定制化开发中,在一些产品定制化需求中,也是有很多重要的功能实现的,比如在某些app的开发中 由于不想被杀掉,所以就不想出现在recent的列表中,因此就需要从recent的列表中,去掉这个app的显示,然后这里有 两种方法实现这个功能,一种是在app中就…...

深度学习之卷积神经网络学习笔记一

1. 引言深度学习是一系列算法的统称&#xff0c;包括卷积神经网络&#xff08;CNN&#xff09;&#xff0c;循环神经网络&#xff08;RNN&#xff09;&#xff0c;自编码器&#xff08;AE&#xff09;&#xff0c;深度置信网络&#xff08;DBN&#xff09;&#xff0c;生成对抗…...

黑盒测试的常用方法

这里我们先设置一个示例,后面的文章中会根据示例来进行讲解 假设有一个程序是判断一个整形数字是否属于1-100 目录 1.等价类法 2.边界值法 3.判定表法 4.场景设计法 5.错误猜测法 6.正交法 1.等价类法 概念:系统性的确定要输入的测试条件的方法可以看出概念非常抽象,那…...

KubeSphere 容器平台高可用:环境搭建与可视化操作指南

Linux_k8s篇 欢迎来到Linux的世界&#xff0c;看笔记好好学多敲多打&#xff0c;每个人都是大神&#xff01; 题目&#xff1a;KubeSphere 容器平台高可用&#xff1a;环境搭建与可视化操作指南 版本号: 1.0,0 作者: 老王要学习 日期: 2025.06.05 适用环境: Ubuntu22 文档说…...

在软件开发中正确使用MySQL日期时间类型的深度解析

在日常软件开发场景中&#xff0c;时间信息的存储是底层且核心的需求。从金融交易的精确记账时间、用户操作的行为日志&#xff0c;到供应链系统的物流节点时间戳&#xff0c;时间数据的准确性直接决定业务逻辑的可靠性。MySQL作为主流关系型数据库&#xff0c;其日期时间类型的…...

Python爬虫实战:研究feedparser库相关技术

1. 引言 1.1 研究背景与意义 在当今信息爆炸的时代,互联网上存在着海量的信息资源。RSS(Really Simple Syndication)作为一种标准化的信息聚合技术,被广泛用于网站内容的发布和订阅。通过 RSS,用户可以方便地获取网站更新的内容,而无需频繁访问各个网站。 然而,互联网…...

基于Uniapp开发HarmonyOS 5.0旅游应用技术实践

一、技术选型背景 1.跨平台优势 Uniapp采用Vue.js框架&#xff0c;支持"一次开发&#xff0c;多端部署"&#xff0c;可同步生成HarmonyOS、iOS、Android等多平台应用。 2.鸿蒙特性融合 HarmonyOS 5.0的分布式能力与原子化服务&#xff0c;为旅游应用带来&#xf…...

抖音增长新引擎:品融电商,一站式全案代运营领跑者

抖音增长新引擎&#xff1a;品融电商&#xff0c;一站式全案代运营领跑者 在抖音这个日活超7亿的流量汪洋中&#xff0c;品牌如何破浪前行&#xff1f;自建团队成本高、效果难控&#xff1b;碎片化运营又难成合力——这正是许多企业面临的增长困局。品融电商以「抖音全案代运营…...

【SQL学习笔记1】增删改查+多表连接全解析(内附SQL免费在线练习工具)

可以使用Sqliteviz这个网站免费编写sql语句&#xff0c;它能够让用户直接在浏览器内练习SQL的语法&#xff0c;不需要安装任何软件。 链接如下&#xff1a; sqliteviz 注意&#xff1a; 在转写SQL语法时&#xff0c;关键字之间有一个特定的顺序&#xff0c;这个顺序会影响到…...

DIY|Mac 搭建 ESP-IDF 开发环境及编译小智 AI

前一阵子在百度 AI 开发者大会上&#xff0c;看到基于小智 AI DIY 玩具的演示&#xff0c;感觉有点意思&#xff0c;想着自己也来试试。 如果只是想烧录现成的固件&#xff0c;乐鑫官方除了提供了 Windows 版本的 Flash 下载工具 之外&#xff0c;还提供了基于网页版的 ESP LA…...

QT: `long long` 类型转换为 `QString` 2025.6.5

在 Qt 中&#xff0c;将 long long 类型转换为 QString 可以通过以下两种常用方法实现&#xff1a; 方法 1&#xff1a;使用 QString::number() 直接调用 QString 的静态方法 number()&#xff0c;将数值转换为字符串&#xff1a; long long value 1234567890123456789LL; …...

LINUX 69 FTP 客服管理系统 man 5 /etc/vsftpd/vsftpd.conf

FTP 客服管理系统 实现kefu123登录&#xff0c;不允许匿名访问&#xff0c;kefu只能访问/data/kefu目录&#xff0c;不能查看其他目录 创建账号密码 useradd kefu echo 123|passwd -stdin kefu [rootcode caozx26420]# echo 123|passwd --stdin kefu 更改用户 kefu 的密码…...

NPOI操作EXCEL文件 ——CAD C# 二次开发

缺点:dll.版本容易加载错误。CAD加载插件时&#xff0c;没有加载所有类库。插件运行过程中用到某个类库&#xff0c;会从CAD的安装目录找&#xff0c;找不到就报错了。 【方案2】让CAD在加载过程中把类库加载到内存 【方案3】是发现缺少了哪个库&#xff0c;就用插件程序加载进…...