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 的核心是单向数据流,它遵循以下流程:

  1. 用户操作产生 Intent
  2. Intent 被 ViewModel 处理
  3. ViewModel 根据 Intent 更新状态
  4. 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 优势

  1. 单向数据流:数据流动方向明确,易于理解和调试
  2. 可测试性:ViewModel 不依赖 View,单元测试更加简单
  3. 状态可追溯:所有状态变化都可以追踪,便于定位问题
  4. 解耦彻底:View 和 Model 完全解耦,职责更加清晰
  5. 一致性:所有状态更新都遵循相同的模式,代码风格统一

4.2 挑战

  1. 学习曲线:相较于 MVVM,MVI 的概念更加抽象,需要一定的时间来理解
  2. 初期开发效率:需要编写更多的代码(Intent 和 State),初期开发效率可能较低
  3. 状态管理复杂度:在复杂场景下,状态管理可能变得复杂,需要合理设计状态结构

五、总结

MVI 架构通过引入单向数据流和不可变状态,为 Android 开发带来了一种更加清晰、可测试、可维护的代码结构。虽然初期学习和开发成本较高,但在大型项目中,MVI 的优势将得到充分体现。

如果你正在寻找一种能够彻底解决 View 和 Model 耦合问题的架构模式,那么 MVI 绝对值得一试。通过遵循 MVI 的设计原则,你可以构建出更加健壮、易于维护的 Android 应用。

希望本文能够帮助你理解 MVI 架构的核心概念和实现方式,让你在 Android 开发的道路上更进一步。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.pswp.cn/bicheng/82490.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【YOLOv8-pose部署至RK3588】模型训练→转换RKNN→开发板部署

已在GitHub开源与本博客同步的YOLOv8_RK3588_object_pose 项目&#xff0c;地址&#xff1a;https://github.com/A7bert777/YOLOv8_RK3588_object_pose 详细使用教程&#xff0c;可参考README.md或参考本博客第六章 模型部署 文章目录 一、项目回顾二、文件梳理三、YOLOv8-pose…

集成30+办公功能的实用工具

软件介绍 本文介绍的软件是千峰办公助手。 软件功能概述与开发目的 千峰办公助手集成了自动任务、系统工具、文件工具、PDF工具、OCR图文识别、文字处理、电子表格七个模块&#xff0c;拥有30余项实用功能。作者开发该软件的目的是解决常见办公痛点&#xff0c;把机械操作交…

IDEA启动报错:Cannot invoke “org.flowable.common.engine.impl.persistence.ent

1.问题 项目启动报错信息 java.lang.NullPointerException: Cannot invoke "org.flowable.common.engine.impl.persistence.ent 2.问题解析 出现这个问题是在项目中集成了Flowable或Activiti工作流&#xff0c;开启自动创建工作流创建的表&#xff0c;因为不同环境的数据…

网络安全--PHP第三天

今天学习文件上传的相关知识 上传的前端页面如下 upload.html <!DOCTYPE html> <html lang"zh-CN"> <head><meta charset"UTF-8"><meta name"viewport" content"widthdevice-width, initial-scale1.0"&g…

【愚公系列】《生产线数字化设计与仿真》004-颜色分类站仿真(基础概念)

🌟【技术大咖愚公搬代码:全栈专家的成长之路,你关注的宝藏博主在这里!】🌟 📣开发者圈持续输出高质量干货的"愚公精神"践行者——全网百万开发者都在追更的顶级技术博主! 👉 江湖人称"愚公搬代码",用七年如一日的精神深耕技术领域,以"…

基于 uni-app + <movable-view>拖拽实现的标签排序-适用于微信小程序、H5等多端

在实际业务中&#xff0c;我们经常遇到「标签排序」或「菜单调整」的场景。微信小程序原生的 movable-view 为我们提供了一个简单、高效的拖拽能力&#xff0c;结合 Vue3 uni-app 的组合&#xff0c;我们可以实现一个体验良好的标签管理界面。 核心组件&#xff1a;<movab…

一些较好的学习方法

1、网上有一些非常经典的电路&#xff0c;而且有很多视频博主做了详细的讲解。 2、有一部分拆解的UP主&#xff0c;拆解后会还原该器件的原理图&#xff0c;并一步步做讲解。 3、有两本书&#xff0c;数电、模电&#xff0c;这两本书中的内容很多都值得学习。 5、某宝上卖的…

《1.1_4计算机网络的分类|精讲篇|附X-mind思维导图》

网络相关知识 按使用范围分类 公用网 由电信部门或其他提供通信服务的经营部门组建、管理和控制&#xff0c;向全社会提供服务的网络。 专用网 由某个单位或部门组建、仅供本单位或部门内部使用的网络。 按传输介质分类 有线网络 如&#xff1a;双绞线、同轴电缆、光纤…

Git 和 GitHub 学习指南本地 Git 配置、基础命令、GitHub 上传流程、企业开发中 Git 的使用流程、以及如何将代码部署到生产服务器

Windows 上 Git 安装与配置 下载安装&#xff1a;访问 Git 官方网站下载适用于 Windows 的安装程序。运行安装包时会出现许可协议、安装目录、组件选择等界面&#xff08;如下图&#xff09;。在“Select Components”页面建议勾选 Git Bash Here 等选项&#xff0c;以便在资源…

航空航天领域对滚珠丝杆的精度要求有多高?

航空航天领域对滚珠丝杆的精度要求非常高&#xff0c;尤其是飞行器、火箭和卫星等载具的导航和定位系统都需要高精度的滚珠丝杆&#xff0c;以确保高精度的位置控制和稳定的导航性能。那么&#xff0c;航空航天领域对滚珠丝杆的精度要求有多高&#xff1f; 1、定位精度&#xf…

技术篇-2.5.Matlab应用场景及开发工具安装

Matlab 在数学建模和数值分析等领域具有无可替代的地位。它几乎涵盖所有常见数学算法的内置函数库&#xff0c;使得从数据预处理、方程求解到优化算法的实现&#xff0c;无需编写大量底层代码即可快速完成&#xff1b;同时&#xff0c;Matlab 强大的可视化能力&#xff0c;可以…

Vtk概览1

vtk环境搭建 见&#xff08;VTK开发环境配置(Visual Studio C)-详细图文教程-CSDN博客&#xff09; 在学习vtk图形图像进阶的第二章时&#xff0c;通过vs2022建的控制台程序&#xff0c;编写运行示例2.1 发现 不显示图像。 #include <iostream> #include<vtkRenderW…

【数据集】基于ubESTARFM法的100m 地温LST数据集(澳大利亚)

目录 数据概述一、输入数据与处理二、融合算法1. ESTARFM(Enhanced STARFM)2. ubESTARFM(Unbiased ESTARFM)代码实现数据下载参考根据论文《Generating daily 100 m resolution land surface temperature estimates continentally using an unbiased spatiotemporal fusion…

Lucide:一款精美的开源矢量图标库,前端图标新选择

名人说:博观而约取,厚积而薄发。——苏轼《稼说送张琥》 创作者:Code_流苏(CSDN)(一个喜欢古诗词和编程的Coder😊) 目录 一、前言:为何选择 Lucide?二、Lucide 是什么?1. 基本介绍2. Lucide vs Feather三、如何在项目中使用 Lucide?1. 安装图标包(以 React 为例)2…

BeanUtil和BeanUtils有什么区别

BeanUtil 和 BeanUtils 是两个常见的工具类&#xff0c;通常用于 Java 开发中处理对象之间的属性复制或转换。它们的功能可能看起来相似&#xff0c;但实际上它们来自不同的库&#xff0c;并且在实现细节和使用方式上存在一些差异。 以下是它们的主要区别&#xff1a; 1. 来源…

【CF】Day66——Edu 168.D + CF 853 (Div. 2).C (树 + 二分 + 贪心 | 组合数学)

D. Maximize the Root 题目&#xff1a; 思路&#xff1a; 树上二分&#xff0c;中下题 我们可以发现如果 x 可以&#xff0c;那么 x - 1 肯定也可以&#xff0c;所以可以直接二分答案 具体的&#xff0c;我们每次二分能增加的值 mid &#xff0c;如果 a[i] < mid&#xf…

生成对抗网络(GANs)中的损失函数公式 判别器最优解D^*(x)的推导

https://www.bilibili.com/video/BV1YyHSekEE2 这张图片展示的是生成对抗网络&#xff08;GANs&#xff09;中的损失函数公式&#xff0c;特别是针对判别器&#xff08;Discriminator&#xff09;和生成器&#xff08;Generator&#xff09;的优化目标。让我们用Markdown格式逐…

分布式爬虫架构设计

随着互联网数据的爆炸式增长&#xff0c;单机爬虫已经难以满足大规模数据采集的需求。分布式爬虫应运而生&#xff0c;它通过多节点协作&#xff0c;实现了数据采集的高效性和容错性。本文将深入探讨分布式爬虫的架构设计&#xff0c;包括常见的架构模式、关键技术组件、完整项…

[java]eclipse中windowbuilder插件在线安装

目录 一、打开eclipse 二、打开插件市场 三、输入windowbuilder&#xff0c;点击install 四、进入安装界面 五、勾选我同意... 重启即可 一、打开eclipse 二、打开插件市场 三、输入windowbuilder&#xff0c;点击install 四、进入安装界面 五、勾选我同意... 重启即可

sass,less是什么?为什么要使用他们?

理解 他们都是css的预处理器,允许开发者通过更高级的语法编写css代码(支持变量,嵌套),然后通过编译成css文件 使用原因 结构清晰,便于扩展提高开发效率,便于后期开发维护