Android 架构演进之路:从 MVC 到 MVI,拥抱单向数据流的革命
在移动应用开发的世界里,架构模式的演进从未停歇。从早期的 MVC 到后来的 MVP、MVVM,每一次变革都在尝试解决前一代架构的痛点。而今天,我们将探讨一种全新的架构模式 ——MVI(Model-View-Intent),它借鉴了前端 React 的单向数据流思想,并结合 Android 开发的最佳实践,为我们带来了一种更加清晰、可测试、可维护的代码结构。
一、架构模式的演进历程
1.1 MVC:原始的分层尝试
MVC(Model-View-Controller)是最早被广泛应用的架构模式之一,它将应用分为三个主要部分:
- Model:负责数据和业务逻辑
- View:负责 UI 展示
- Controller:作为 View 和 Model 之间的桥梁,处理用户输入和状态更新
在 Android 中,Activity 通常扮演 Controller 的角色,但随着 UI 逻辑的复杂化,Activity 变得越来越臃肿,违反了单一职责原则。
1.2 MVP:引入 Presenter 层解耦
为了解决 MVC 的问题,MVP(Model-View-Presenter)应运而生:
- Model:数据和业务逻辑
- View:负责 UI 展示,通常是一个接口
- Presenter:处理 UI 逻辑,与 Model 交互
MVP 通过接口将 View 和 Presenter 解耦,使得单元测试更加容易。但随着项目规模增大,Presenter 层可能会变得非常庞大。
1.3 MVVM:数据绑定的力量
MVVM(Model-View-ViewModel)在 MVP 的基础上引入了数据绑定:
- Model:数据和业务逻辑
- View:负责 UI 展示
- ViewModel:暴露数据流,通过数据绑定与 View 交互
MVVM 通过 LiveData、Data Binding 等技术减少了 View 和 ViewModel 之间的耦合,但它仍然存在一些问题,例如双向数据流可能导致的复杂性。
1.4 MVI:单向数据流的革命
MVI(Model-View-Intent)是架构模式的最新演进,它借鉴了 React 的单向数据流思想:
- Model:不可变的状态容器
- View:订阅状态并渲染 UI
- Intent:用户操作转换为 Intent,触发状态更新
MVI 的核心思想是单向数据流和状态不可变性,所有的状态变化都遵循一个可预测的流程,使得代码更加易于理解和调试。
二、MVI 架构的核心概念
2.1 单向数据流
MVI 的核心是单向数据流,它遵循以下流程:
- 用户操作产生 Intent
- Intent 被 ViewModel 处理
- ViewModel 根据 Intent 更新状态
- View 订阅状态变化并更新 UI
这种单向流动使得数据的流向变得清晰可追溯,大大降低了代码的复杂性。
2.2 不可变状态
在 MVI 中,状态是不可变的。每当有新的状态产生时,不会修改原有状态,而是创建一个新的状态对象。这样可以确保状态的变化是可追踪的,并且简化了状态管理。
2.3 状态容器
ViewModel 作为状态容器,负责管理和更新应用状态。它接收 Intent 并根据这些 Intent 生成新的状态,然后将新状态发送给 View。
三、MVI 架构的实现
3.1 基础架构实现
首先,我们需要定义一些基础接口和类来构建 MVI 架构:
kotlin
// BaseIntent.kt
interface BaseIntent {
}// BaseState.kt
interface BaseState {data object Loading : BaseState // 加载中状态data object Complete: BaseState // 处理完成data class Error(val message: String = "", val updateId: Long = System.currentTimeMillis()): BaseState // 异常
}// BaseViewModel.kt
import android.content.Context
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.launchabstract class BaseViewModel : ViewModel() {/*** 获取上下文*/protected fun getContext(): Context = BaseApplication.instance.applicationContext// 接收来自视图层的意图(Intent)val intentsFlow = MutableSharedFlow<BaseIntent>()// 用于向视图层发送状态更新,类型指定为BaseStateval stateFlow = MutableStateFlow<BaseState?>(null)init {consumption()}/*** 分发消息*/private fun consumption() {viewModelScope.launch {intentsFlow.collect { intent ->handleIntents(intent)}}}abstract fun handleIntents(intent: BaseIntent)// 方法用于向视图层发送新的BaseState类型的状态protected fun updateState(newState: BaseState) {viewModelScope.launch {delay(100) // 防止消息太快,丢失stateFlow.value = newState}}
}// BaseActivity.kt
import android.util.Log
import android.view.LayoutInflater
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
import androidx.viewbinding.ViewBinding
import kotlinx.coroutines.launch
import kotlin.reflect.KClassabstract class BaseActivity<VM : BaseViewModel, VB : ViewBinding>(override val vbInflater: (LayoutInflater) -> VB
) : AppCompatActivity() {val tag = this::class.simpleNameprivate val _binding by lazy { vbInflater(layoutInflater) }val binding get() = _bindingopen val viewModel: VM by lazy {initializeViewModel()}override fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)setContentView(binding.root)setupObservers()setupIntents()bindClickListener()}/*** 初始化ViewModel*/private fun initializeViewModel(): VM {val classifier = this::class.supertypes[0].arguments[0].type?.classifierLog.d(this::class.simpleName, "initializeViewModel()----->${classifier}")val viewModelClass = (classifier as? KClass<VM>)?.java?: throw IllegalArgumentException("Invalid ViewModel class")return ViewModelProvider(this)[viewModelClass]}/*** 分发状态*/private fun setupObservers() {lifecycleScope.launch {viewModel.stateFlow.collect { state ->Log.d(tag, "setupObservers()---->$state")render(state)}}}// 抽象方法,由子类实现来根据不同的BaseState状态渲染UIabstract fun render(state: BaseState?)// 辅助方法,用于发送BaseIntent类型的意图到ViewModelprotected fun sendIntent(intent: BaseIntent) {lifecycleScope.launch {viewModel.intentsFlow.emit(intent)}}// 设置意图,由子类实现open fun setupIntents() {}// 绑定点击事件,由子类实现open fun bindClickListener() {}
}
3.2 登录功能的 MVI 实现
接下来,我们以登录功能为例,展示如何使用 MVI 架构实现一个完整的功能模块。
首先是布局文件:
xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"android:layout_width="match_parent"android:layout_height="match_parent"xmlns:app="http://schemas.android.com/apk/res-auto"><androidx.appcompat.widget.AppCompatEditTextandroid:id="@+id/et_username"app:layout_constraintTop_toTopOf="parent"app:layout_constraintBottom_toTopOf="@+id/et_password"app:layout_constraintVertical_chainStyle="packed"android:layout_marginStart="12dp"android:layout_marginEnd="12dp"android:layout_marginBottom="12dp"android:layout_width="match_parent"android:hint="手机号"android:layout_height="wrap_content"/><androidx.appcompat.widget.AppCompatEditTextandroid:id="@+id/et_password"android:layout_width="match_parent"app:layout_constraintTop_toBottomOf="@+id/et_username"app:layout_constraintBottom_toTopOf="@+id/btn_login"android:hint="密码"android:layout_margin="12dp"app:layout_constraintVertical_chainStyle="packed"android:layout_height="wrap_content"/><androidx.appcompat.widget.AppCompatButtonandroid:id="@+id/btn_login"android:layout_width="match_parent"android:layout_height="wrap_content"android:layout_marginTop="24dp"android:layout_marginStart="12dp"android:layout_marginEnd="12dp"android:textColor="@color/white"android:text="Login"android:background="@drawable/selector_rounded_button"app:layout_constraintBottom_toBottomOf="parent"app:layout_constraintTop_toBottomOf="@+id/et_password"/><com.google.android.material.progressindicator.CircularProgressIndicatorandroid:id="@+id/progressBar"app:layout_constraintLeft_toLeftOf="parent"app:layout_constraintRight_toRightOf="parent"app:layout_constraintTop_toTopOf="parent"app:layout_constraintBottom_toBottomOf="parent"android:indeterminateTint="@color/main_color"app:indicatorColor="@color/main_color"app:trackColor="@color/main_color"android:layout_width="40dp"android:visibility="gone"android:layout_height="40dp"/></androidx.constraintlayout.widget.ConstraintLayout>
然后定义登录相关的 Intent、State 和 ViewModel:
kotlin
// LoginIntent.kt
sealed class LoginIntent: BaseIntent {data class EntryPhone(val phone: String): LoginIntent() // 手机号的意图data class EntryPassword(val password: String): LoginIntent() // 密码的意图data object Login: LoginIntent() // 点击登录
}// LoginViewState.kt
sealed class LoginViewState: BaseState {data class LoginStatus(val highlight: Boolean) : LoginViewState()// 高亮状态data object Loading : LoginViewState() // 加载中状态data class Success(val user: UserBean?) : LoginViewState() // 成功登录状态data class Error(val message: String) : LoginViewState() // 登录错误状态
}// LoginViewModel.kt
@HiltViewModel
class LoginViewModel @Inject constructor (private val repository: LoginRepository,
) : BaseViewModel() {private var phone: String = ""private var password: String = ""// 设置登录按钮是否可用private fun setLoginButtonEnabled(enabled: Boolean) {updateState(LoginViewState.LoginStatus(enabled))}/*** 调用登录*/private fun login() {updateState(LoginViewState.Loading)viewModelScope.launch {repository.login(phone, password).onSuccess {updateState(LoginViewState.Success(it))}.onFailure {updateState(LoginViewState.Error(it.message.toString()))}}}/*** 处理意图*/override fun handleIntents(intent: BaseIntent) {when(intent) {is LoginIntent.EntryPhone -> {phone = intent.phonesetLoginButtonEnabled(phone.length >= 6 && password.length >= 6)}is LoginIntent.EntryPassword -> {password = intent.passwordsetLoginButtonEnabled(password.length >= 6 && phone.length >= 6)}is LoginIntent.Login -> {login()}}}
}
最后是 Activity 的实现:
kotlin
// LoginActivity.kt
@AndroidEntryPoint
class LoginActivity : BaseActivity<LoginViewModel, ActivityLoginBinding>(ActivityLoginBinding::inflate
) {/*** 监听状态*/override fun render(state: BaseState?) {when(state) {is LoginViewState.LoginStatus -> {setLoginButtonEnabled(state.highlight)}is LoginViewState.Loading -> {showLoading()}is LoginViewState.Success -> {if (state.user != null) {showSuccess(state.user)}}is LoginViewState.Error -> {showError(state.message)}}}/*** View视图与Intent意图进行绑定*/override fun setupIntents() {binding.etUsername.addTextChangedListener { text ->sendIntent(LoginIntent.EntryPhone(text.toString()))}binding.etPassword.addTextChangedListener { text ->sendIntent(LoginIntent.EntryPassword(text.toString()))}binding.etUsername.setText("1234567890")binding.etPassword.setText("54321#")}/*** 设置点击事件*/override fun bindClickListener() {super.bindClickListener()binding.btnLogin.setOnClickListener {sendIntent(LoginIntent.Login)}}/*** 登录成功*/private fun showSuccess(user: UserBean) {binding.progressBar.visibility = View.GONEToast.makeText(this, "Login successful: ${user.userName}", Toast.LENGTH_SHORT).show()}/*** 显示错误信息*/override fun showError(errorMessage: String?) {super.showError(errorMessage)binding.progressBar.visibility = View.GONEToast.makeText(this, errorMessage, Toast.LENGTH_SHORT).show()}// 登录按钮是否可用private fun setLoginButtonEnabled(enabled: Boolean) {binding.btnLogin.isEnabled = enabled}
}
四、MVI 架构的优势与挑战
4.1 优势
- 单向数据流:数据流动方向明确,易于理解和调试
- 可测试性:ViewModel 不依赖 View,单元测试更加简单
- 状态可追溯:所有状态变化都可以追踪,便于定位问题
- 解耦彻底:View 和 Model 完全解耦,职责更加清晰
- 一致性:所有状态更新都遵循相同的模式,代码风格统一
4.2 挑战
- 学习曲线:相较于 MVVM,MVI 的概念更加抽象,需要一定的时间来理解
- 初期开发效率:需要编写更多的代码(Intent 和 State),初期开发效率可能较低
- 状态管理复杂度:在复杂场景下,状态管理可能变得复杂,需要合理设计状态结构
五、总结
MVI 架构通过引入单向数据流和不可变状态,为 Android 开发带来了一种更加清晰、可测试、可维护的代码结构。虽然初期学习和开发成本较高,但在大型项目中,MVI 的优势将得到充分体现。
如果你正在寻找一种能够彻底解决 View 和 Model 耦合问题的架构模式,那么 MVI 绝对值得一试。通过遵循 MVI 的设计原则,你可以构建出更加健壮、易于维护的 Android 应用。
希望本文能够帮助你理解 MVI 架构的核心概念和实现方式,让你在 Android 开发的道路上更进一步。
相关文章:
Android 架构演进之路:从 MVC 到 MVI,拥抱单向数据流的革命
在移动应用开发的世界里,架构模式的演进从未停歇。从早期的 MVC 到后来的 MVP、MVVM,每一次变革都在尝试解决前一代架构的痛点。而今天,我们将探讨一种全新的架构模式 ——MVI(Model-View-Intent),它借鉴了…...

[低代码表单生成器设计基础]ElementUI中Layout布局属性Form表单属性详解
Layout 布局 ElementUI 的 Layout 布局系统基于 24 栏栅格设计,提供了灵活的响应式布局能力,适用于各种页面结构的构建。(CSDN) 📐 基础布局结构 ElementUI 的布局由 <el-row>(行)和 <el-col>࿰…...
数据结构7——二叉树
一、二叉树的定义与性质 1.定义 首先是树形结构,每个节点最多有2棵树,二叉树的子树有左右之分,不能颠倒。 2.性质 (1)二叉树的第i层,最多有2的(i-1)次幂。 (2)深度为k࿰…...

从“被动养老”到“主动健康管理”:平台如何重构代际关系?
在老龄化与数字化交织的背景下,代际关系的重构已成为破解养老难题的关键。 传统家庭养老模式中,代际互动多表现为单向的“赡养-被赡养”关系。 而智慧养老平台的介入,通过技术赋能、资源整合与情感连接,正在推动代际关系向“协作…...
Java 中的 synchronized 和 Lock:如何保证线程安全
Java 中的 synchronized 和 Lock:如何保证线程安全 引言 在 Java 多线程编程中,线程安全是一个核心问题。当多个线程同时访问共享资源时,可能会导致数据不一致或其他不可预期的结果。synchronized关键字和Lock接口是 Java 中实现线程同步的…...

贪心算法应用:最大匹配问题详解
Java中的贪心算法应用:最大匹配问题详解 贪心算法是一种在每一步选择中都采取当前状态下最优的选择,从而希望导致结果是全局最优的算法策略。在Java中,贪心算法可以应用于多种问题,其中最大匹配问题是一个经典的应用场景。下面我将从基础概念到具体实现,全面详细地讲解贪…...

爬虫IP代理效率优化:策略解析与实战案例
目录 一、代理池效率瓶颈的根源分析 二、六大核心优化策略 策略1:智能IP轮换矩阵 策略2:连接复用优化 策略3:动态指纹伪装 策略4:智能重试机制 三、典型场景实战案例 案例1:电商价格监控系统 案例2:…...

豆瓣电视剧数据工程实践:从爬虫到智能存储的技术演进(含完整代码)
通过网盘分享的文件:资料 链接: https://pan.baidu.com/s/1siOrGmM4n-m3jv95OCea9g?pwd4jir 提取码: 4jir 1. 引言 1.1 选题背景 在影视内容消费升级背景下,豆瓣电视剧榜单作为国内最具影响力的影视评价体系,其数据价值体现在:…...
【HW系列】—C2远控服务器(webshell链接工具, metasploit、cobaltstrike)的漏洞特征流量特征
文章目录 蚁剑、冰蝎、哥斯拉一、蚁剑(AntSword)流量特征二、冰蝎(Behinder)流量特征三、哥斯拉(Godzilla)流量特征 metasploit、cobaltstrike一、Metasploit流量特征二、CobaltStrike流量特征三、检测与防…...
5.28 孔老师 nlp讲座
本次讲座主要介绍了语言模型的起源、预训练模型以及大语言模型(需要闫老师后讲)等内容。首先,语言模型的起源可以追溯到语音识别中的统计语言模型,通过估计声学参数串产生文字串的概率来找到最大概率的文字串。然后,介…...

基于微信小程序的漫展系统的设计与实现
博主介绍:java高级开发,从事互联网行业六年,熟悉各种主流语言,精通java、python、php、爬虫、web开发,已经做了六年的毕业设计程序开发,开发过上千套毕业设计程序,没有什么华丽的语言࿰…...
打卡day39
一、 图像数据的介绍 1.1 灰度图像 # 先继续之前的代码 import torch import torch.nn as nn import torch.optim as optim from torch.utils.data import DataLoader , Dataset # DataLoader 是 PyTorch 中用于加载数据的工具 from torchvision import datasets, transforms…...

基于Web的分布式图集管理系统架构设计与实践
引言:为什么需要分布式图集管理? 在现代Web图形应用中,纹理图集(Texture Atlas)技术是优化渲染性能的关键手段。传统的图集制作流程通常需要美术人员使用专业工具(如TexturePacker)离线制作&am…...

mysql执行sql语句报错事务锁住
报错情况 1205 - Lock wait timeout exceeded; try restarting transaction先找出长时间运行的事务 SELECT * FROM information_schema.INNODB_TRX ORDER BY trx_started ASC;终止长时间运行的事务 KILL [PROCESS_ID];...

Java消息队列应用:Kafka、RabbitMQ选择与优化
Java消息队列应用:Kafka、RabbitMQ选择与优化 在Java应用领域,消息队列是实现异步通信、应用解耦、流量削峰等重要功能的关键组件。Kafka和RabbitMQ作为两种主流的消息队列技术,各有特点和适用场景。本文将深入探讨Kafka和RabbitMQ在Java中的…...

零基础设计模式——结构型模式 - 组合模式
第三部分:结构型模式 - 组合模式 (Composite Pattern) 在学习了桥接模式如何分离抽象和实现以应对多维度变化后,我们来探讨组合模式。组合模式允许你将对象组合成树形结构来表现“整体-部分”的层次结构。组合模式使得用户对单个对象和组合对象的使用具…...
额度年审领域知识讲解
金融领域的“额度年审”是一个非常重要的常规性工作。它指的是金融机构(主要是银行)对其授予客户的各种信用额度或授信额度,在授信有效期内(通常是一年)进行周期性的重新评估、审查和确认的过程。 核心目的࿱…...

腾讯云国际站可靠性测试
在数字化转型加速的今天,企业对于云服务的依赖已从“可选”变为“必需”。无论是跨境电商的实时交易,还是跨国企业的数据协同,云服务的可靠性直接决定了业务连续性。作为中国领先的云服务提供商,腾讯云国际站(Tencent …...

自定义异常小练习
在开始之前,让我们高喊我们的口号: 键盘敲烂,年薪百万! 目录 键盘敲烂,年薪百万! 异常综合练习: 自定义异常 异常综合练习: 自定义异常: 定义异常类写继承关系空参构造带参构造 自定…...

SpringBoot整合MinIO实现文件上传
使用Spring Boot与JSP和MinIO(一个开源对象存储系统,兼容Amazon S3)进行集成,您可以创建一个Web应用来上传、存储和管理文件。以下是如何将Spring Boot、JSP和MinIO集成的基本步骤: 这个是minio正确启动界面 这个是min…...

基于面向对象设计的C++日期推算引擎:精准高效的时间运算实现与运算重载工程化实践
前引: 在软件开发中,时间与日期的处理是基础但极具挑战性的任务。传统的手工日期运算逻辑往往面临闰年规则、月份天数动态变化、时区转换等复杂场景的容错难题,且代码冗余度高、可维护性差。本文将深入探讨如何利用C的面向对象特性与成员函数…...

如何把 Microsoft Word 中所有的汉字字体替换为宋体?
Ctrl H ,然后,点击更多,勾选使用通配符,查找内容中填入 [一-龥]{1,}, 这是 Word 通配符匹配汉字的经典写法(匹配 Unicode 范围内的 CJK 汉字)。 然后, “替换为”留空,点…...
02. [Python+Golang+PHP]三数之和,多种语言实现最优解demo
一、问题描述:三数之和 给你一个整数数组 nums ,判断是否存在三元组 [nums[i], nums[j], nums[k]] 满足 i ! j、i ! k 且 j ! k ,同时还满足 nums[i] nums[j] nums[k] 0 。请你返回所有和为 0 且不重复的三元组。 注意:答案中…...
MongoDB选择理由
1.简介 MongoDB是一个基于分布式文件存储的数据库由C语言编写,旨在为WEB应用提供可扩展的高性能数据存储解决方案。MongoDB是一个介于关系数据库和非关系数据库之间的产品,是非关系数据库当中功能最丰富,最像关系数据库的。Mongo最大的特点是…...

倚光科技在二元衍射面加工技术上的革新:引领光学元件制造新方向
倚光科技二元衍射面加工技术(呈现出细腻的光碟反射纹路) 在光学元件制造领域,二元衍射面的加工技术一直是行业发展的关键驱动力之一。其精准的光相位调制能力,在诸多前沿光学应用中扮演着不可或缺的角色。然而,长期以来…...

驱动开发(2)|鲁班猫rk3568简单GPIO波形操控
上篇文章写了如何下载内核源码、编译源码的详细步骤,以及一个简单的官方demo编译,今天分享一下如何根据板子的引脚写自己控制GPIO进行高低电平反转。 想要控制GPIO之前要学会看自己的引脚分布图,我用的是鲁班猫RK3568,引脚分布图如…...

《软件工程》第 3 章 -需求工程概论
在软件工程的开发流程中,需求工程是奠定项目成功基础的关键环节。它专注于获取、分析、定义和管理软件需求,确保开发出的软件能真正满足用户需求。接下来,我们将按照目录内容,结合 Java 代码和实际案例,深入讲解需求工…...

VMware-MySQL主从
MySQL主从 服务器信息 服务器类型角色主机地址主机名称虚拟机master192.168.40.128test-1虚拟机slave192.168.40.129test-2 Master 配置(192.168.40.128) 删除自动生成的配置 /var/lib/mysql/auto.cnf [roottest-1 ~]# rm -rf /var/lib/mysql/auto.…...
ArcGIS Pro 3.4 二次开发 - 几何
环境:ArcGIS Pro SDK 3.4 + .NET 8 文章目录 几何1 空间参考1.1 从已知ID构建空间参考1.2 从字符串构建空间参考1.3 使用 WGS84 空间参考1.4 使用已知ID构建带有垂直坐标系的空间参考1.5 使用垂直坐标系从字符串构建SpatialReference1.6 使用自定义投影坐标系(PCS)构建空间参…...

2023-ICLR-ReAct 首次结合Thought和Action提升大模型解决问题的能力
关于普林斯顿大学和Google Research, Brain Team合作的一篇文章, 在语言模型中协同Reasoning推理和Action行动。 论文地址:https://arxiv.org/abs/2210.03629 代码:https://github.com/ysymyth/ReAct.git 其他复现 langchain :https://pytho…...