在Android面试的时候,软件架构是一个比较高频的话题,虽然我基本能搞清,但是如果问得特别深、问得特别刁钻的话,有时也会很吃力,而网上对于MVC、MVP、MVVM、MVI这几种架构的介绍绝大多数都是抄来抄去,虽然有的也有代码、有图,但是还是让人看不明白,所以我决定:深挖到底!
1. MVC
话说那还是软件开发的上古时期,公元1980年代。那时候哪有什么正经的"操作系统"啊?应用程序和操作系统的边界就像"有关部门"一样模糊不清。你写一个程序,甚至就得自己画窗口、自己画鼠标箭头。什么窗口管理器、消息队列、按钮控件?不存在的,那都是后来文明世界的产物!
所以那会儿的程序员,窗口实现、UI实现、业务逻辑、操作系统的活统统都要自己来干!这是一群软件开发的祖师爷,软件开发的探路者,没有人指导他们该怎么做。你现在回想自己初学编程时,是不是也干过把一堆乱七八糟的代码全都写在同一个文件里的事?没错,这群祖师爷也这么干的!管你什么UI什么业务逻辑,统统炖成八宝粥!------永远在一起。比如,一个文件,它既负责界面的绘制,又负责数据的保存和处理。你想改个按钮的长度?说不定连底层的业务逻辑都得跟着翻车,牵一发而动全身,改个UI能改出满屏bug来。
UI展示、业务逻辑混杂在一起,难以维护、难以调试、难以扩展、难以修改
就在这混沌之中,一位叫 Trygve Reenskaug 的大仙终于看不下去了,他登高一呼:"咱不能再这么过啦!"。 一声巨吼,混沌初开!大仙要给这个世界立规矩了!
Model: 负责数据和业务逻辑。比如从哪获取数据、怎样处理数据。它不关心任何UI的东西。
View: 负责界面显示。具体来讲是展示Model的数据。
Controller:响应用户输入,连接 Model 和 View ,是整个程序的控制中心,总揽全局。
MVC的协作方式(经典Smalltalk版本)
(1)用户操作鼠标/键盘 → Controller 捕获事件。
(2)Controller根据事件内容,调用 Model 的某个方法修改数据。
(3)Model通过观察者模式(Observer)通知所有注册的 View:"我的数据变了"。
(4)View收到通知后,从Model中重新读取数据,并更新屏幕上的显示。
这种模式带来一个深远的影响:Model完全不依赖View和Controller,View和Controller依赖Model。 这种单向依赖使得Model可以独立开发、测试,也使得View可以灵活替换。
MVC诞生于一个图形界面刚刚萌芽、没有任何现成框架的年代。应用程序必须自己处理原始输入、自己绘制光标、自己管理所有界面元素。 在这样的环境下,MVC通过将数据(Model)、呈现(View)、交互(Controller)三者分离,解决了代码混乱、难以复用和测试的问题,为现代图形界面架构铺平了道路。
下面就是在操作系统连基础控件都不提供的年代MVC的实现示例
//Model存储数据(这里只用一个int值演示),数据改变后就用观察者模式通知观察者
class CounterModel {
private int value = 0
private List observers = []
method getValue() { return value }
method increment() {
value = value + 1
notifyObservers() // 数据改变,通知所有观察者
}
method addObserver(observer) {
observers.add(observer)
}
private method notifyObservers() {
for each observer in observers {
observer.update() // 观察者收到更新通知
}
}
}
// View 是一个抽象基类,表示屏幕上一个区域。一个View其实就是一个控件。
abstract class View {
int x, y, width, height // 区域位置和大小
CounterModel model // 关联的Model
method new(model, x, y, w, h) {
this.model = model
this.x = x; this.y = y; this.width = w; this.height = h
model.addObserver(this) // 注册为Model的观察者
}
// 当Model变化时,框架会调用这个方法
method update() {
draw() // 重新绘制自己
}
// 子类实现具体的绘制逻辑
abstract method draw()
}
// 数字显示控件
class TextView extends View {
method draw() {
int val = model.getValue()
// 在 (x, y) 位置清除旧区域并绘制新数字
// 这里使用底层绘图原语(如画矩形、画文本)
drawRect(x, y, width, height, background)
drawText(x+5, y+5, val.toString())
}
}
// 按钮控件
class ButtonView extends View {
method draw() {
// 绘制按钮外观(边框、背景、文字)
drawRect(x, y, width, height, buttonColor)
drawRectBorder(x, y, width, height, black)
drawText(x+5, y+5, "+")
}
// 处理点击(由Controller调用)
method handleClick() {
model.increment() // 直接修改Model
}
}
// Controller负责整个应用的输入分发和主循环
class ApplicationController {
List views = [] // 所有视图(TextView, ButtonView等)
CounterModel model
method new() {
model = new CounterModel()
// 创建视图,指定屏幕位置
TextView textView = new TextView(model, 10, 10, 100, 30)
ButtonView button = new ButtonView(model, 10, 50, 50, 30)
views.add(textView)
views.add(button)
// 初始绘制所有视图
for each view in views {
view.draw()
}
}
// 主循环:应用程序的生命周期
method run() {
while (true) {
// 1. 等待用户输入(鼠标移动/点击,键盘)
Event event = waitForInput() // 底层从硬件获取原始事件
// 2. 如果是鼠标点击
if (event.type == MOUSE_DOWN) {
// 遍历所有视图,找出哪个视图包含点击坐标
View clickedView = null
for each view in views {
if (event.x >= view.x and event.x <= view.x+view.width and
event.y >= view.y and event.y <= view.y+view.height) {
clickedView = view
break
}
}
// 如果点中了一个按钮视图,调用其点击处理
if (clickedView is ButtonView) {
((ButtonView)clickedView).handleClick()
}
// 其他视图可根据需要处理点击(如文本视图可能响应双击编辑等)
}
// 3. 处理其他输入(键盘、鼠标移动等)
// 例如鼠标移动时可能需要改变光标形状,由Controller更新光标绘制
// ...
}
}
// 模拟底层获取原始输入的方法
method waitForInput() -> Event {
// 这里会阻塞,直到有鼠标移动、按键或点击
// 返回包含坐标、按键类型等的事件对象
}
}
// 程序入口
main() {
controller = new ApplicationController()
controller.run()
}
看到了吧,最初的MVC典型使用场景是这样的:程序启动后,Controller首先创建各View(View在创建时会调用Model的注册方法把自己注册到Model)然后调用各View的绘制方法,以便让整个界面展示出来,同时Controller也一直在监听操作事件(比如鼠标点击),在收到事件后,Controller就分析该事件应该对应到哪个View,然后调用View的方法以便把这个事件传递给View,然后View继续调用Model的方法,让Model执行业务逻辑,Model执行完毕后数据就得到了更新,就会用观察者的模式告知各View,View于是更新界面。它的数据流动方向是这样的 用户操作 --> Controller --> View --> Model --> View。不过该数据流动方向可不是MVC的强制要求,实际Controller也可以直接传给Model。
2. MVP
时间来到90年代,此时MVC已诞生多个年头了,操作系统的发展已取得了重大跨越式进步,应用程序终于不用再自己操作如何去实现界面了,因为此时操作系统已经提供了窗口、按钮、文本框等等现成的组件,不过也正是因为如此,新的问题出现了:在没有这些现成组件之前,像是鼠标操作、键盘输入等都是由Controller直接监听的,Controller是事件的第一知晓者,它收到后再决定是交给Model处理还是交给View处理,但是后来出现的这些现成组件都是由View来充当事件的第一知晓者,因为它们的实现大都是这样的
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
//处理点击事件。View把流程控制权从C手里夺过来了。
//回想一下MVC的那段长代码,这本该是C的职责呀。
if (xxx) { ... }
model.xxx()
updateUI()
}
});
看到了吧,由于操作系统提供的这些组件的实现方案的原因,原本Controller的活儿被View干了!View同时也承担了Controller的角色!但是......好像也没什么嘛,比如按钮点击后,按钮就直接model.xxx() 让Model去处理,然后把处理后的数据再返回给View,由View展示出来,不就行了?业务简单、代码不多的时候,确实可以!
咱们具体到Android APP开发上,界面布局的XML文件就承担着View的角色,但是XML文件只能作为静态展示(那时还没有jetpack),当需要更新UI的时候,还是需要Activity来通过代码更新才行,所以Activity实际也承担了View的角色;另外,由于Android的组件实现也是这样的------View组件是事件的第一知晓者(监听者),而且很多流程的触发是由Activity的onCreate()、onPause()等生命周期方法触发的,所以Activity也同时承担着Controller的角色!所以,在Android刚刚兴起,应用业务都还不复杂的时候,MVC确实是够用的,只是随着业务增加,Android APP开发也遇到了之前它的那些前辈们同样的问题。MVP迎着这些困难上场了!
**1、V、C耦合严重。**业务越来越复杂,View和Controller代码也越来越多,但是由于View、Controller集于一身,导致文件代码快速膨胀,耦合越来越严重,职责混杂,测试和维护越来越困难。
**2、MVC数据流向多样,不易调试和扩展。**在典型 MVC 实现中,数据可以沿 Controller → Model → View 流动(Controller监听到事件,调用Model处理,View展示结果),也可以 Controller → View → Model → View(Controller监听事件,交给View判断,View交给Model处理,View展示结果)。这种多向依赖在大型应用中容易形成混乱的调用关系,难以梳理清楚数据流向,进而难以调试和扩展。
MVP提出
Model :跟MVC里一样,负责数据和业务逻辑。
View :负责UI绘制、用户交互的原始捕获(即监听点击、键盘输入等)。它尽可能被动,只通过接口暴露方法(如showLoading()、showError(String msg))以便被动更新UI。
Presenter:作为中间层,负责从Model获取数据,对数据进行加工,然后通过调用View接口方法将结果"推送"给View。
下面是示例代码,需求很简单:
-
输入用户名
-
点击登录
-
显示结果(成功 / 失败)
class UserModel {
public boolean login(String username) {
// 模拟业务逻辑
return "admin".equals(username);
}
}interface LoginView {
void showLoading();
void hideLoading();
void showSuccess();
void showError(String msg);
}class LoginPresenter {
private LoginView view; private UserModel model; public LoginPresenter(LoginView view) { this.view = view; this.model = new UserModel(); } public void onLoginClicked(String username) { view.showLoading(); boolean result = model.login(username); view.hideLoading(); if (result) { view.showSuccess(); } else { view.showError("登录失败"); } }}
class LoginActivity implements LoginView {
private LoginPresenter presenter; public LoginActivity() { presenter = new LoginPresenter(this); } public void clickLogin(String username) { // 用户点击按钮 → 交给 Presenter presenter.onLoginClicked(username); } @Override public void showLoading() { System.out.println("加载中..."); } @Override public void hideLoading() { System.out.println("加载结束"); } @Override public void showSuccess() { System.out.println("登录成功!"); } @Override public void showError(String msg) { System.out.println(msg); }}
public class Main {
public static void main(String[] args) {
LoginActivity view = new LoginActivity();view.clickLogin("admin"); // 成功 view.clickLogin("user"); // 失败 }}
咱们把这两种模式放到一起做一个仔细的对比
|----------------------|-------------------------------------------------------------------------------|----------------------------------------------------|
| | MVC | MVP |
| Model | 负责数据和业务逻辑。可以主动通知View更新。 | 负责数据和业务逻辑。不直接和View打交道。 |
| View | 展示数据,可以接收用户输入 | 展示数据,可以接收用户输入 |
| Controller/Presenter | 接收用户输入(从 View 或系统),决定调用哪个 Model 或更新哪个 View,但 View 也可以绕过 Controller 直接操作 Model | 唯一的中介。接收 View 转发的事件,调用 Model,处理数据,调用 View 接口刷新 UI。 |
经过仔细梳理,会发现MVC和MVP有几点关键的区别:
1、MVP的数据流向是明确的。P是V和M的唯一交互通道,V和M之间的数据流动只能通过P。MVP只有一个数据流动方向 V → P → M → P → V 。MVC没有明确的数据流动方向,存在多条可操作路径。
2、MVP中,V是用户交互(点击、键盘输入等)的直接监听者,而经典MVC中C才是直接监听者,后来实际实现时演变为V也可以是直接监听者。
3、实际实现中,MVC的V和C往往耦合在一起,而MVP中V只负责UI展示和事件(点击、键盘输入等)转发,P很独立地负责业务逻辑和一些数据加工逻辑,MVP的分工更明确。
4、由于VC耦合,MVC不易进行单元测试;MVP界限清晰,方便单元测试。
5、与第一点本质相同:MVC中通常由M以观察者模式通知V更新,而往往V就是那个观察者(即V implements Observer),造成M持有V的引用(当然了,如果V不是Observer,而是V创建一个Observer注册进M,M就不会引用V),V也通常会直接调用M(例如点击后直接调用M处理数据),所以V也常会持有M,如果出现了互相引用,就易造成内存泄漏;但在MVP中M把结果反馈给P,再由P通知V更新,即M与V无关联,但P会同时引用V和M。
好了,咱们再来看看,MVP把MVC的弊端都解决了吗?
1、MVC由于VC耦合,导致臃肿,测试和维护越来越困难------MVP明确分工,严格独立,业务多了还可以拆分成多个P。该问题解决。
2、MVC数据流向多样,不易调试和扩展------MVP数据流向明确。该问题解决。
好了,咱们做一个总结吧:
由于后来的GUI机制(即View监听用户输入),导致MVC的View夺取了流程控制权(这本该是C的职责),同时造成V、C耦合在一起,流程控制权由原本C一手操控变成了分散到了各View组件里,业务复杂时,难调试、难维护、难扩展、难测试,MVP把流程控制权重新掌握到P手中,并约束交互路径,使结构重新变得清晰起来。
深度思考
MVP架构中View监听到事件后就转交给P处理,那MVC也可以呀!MVP都是由P做中转,那MVC架构中也可以由C做中转呀!MVP让Activity实现View接口,MVC架构也可以呀!MVC实现混乱,很大程度上应该怪实现者!
这个说法其实是没错的。以Android开发为例,由于它的设计的原因,Activity就是天生自带View+Controller的角色色彩,非常让人(简直是故意引诱似的)容易写着写着就把V和C搞在一起,而上述问题中提出的问题也确实都对,只是MVC架构中并不禁止这样做,但是MVP架构却针对MVC的叫缺点也好漏洞也好,做了严格规定,例如MVP里明确规定就是不让View直接调用Model,但是MVC里允许这样做。所以,MVP可以认为是一种追加了更多附加条件的MVC。
3. MVVM
历史的车轮滚啊滚......浪花淘尽英雄。现在看MVP也不顺眼了!为什么呢?
1、P变得越来越臃肿。 P是V和M的唯一桥梁,V和M之间的所有交互都由P来转达,很多流程控制的逻辑也都有P来控制,尽管也可以拆分成多个小P,但P还是无法避免像MVC里的VC一样变得越来越臃肿。
2、需要定义海量接口,繁琐、易错。 V收到事件要通过接口转给P,P处理后再通过接口调用M,M再通过接口传回P,P再通过接口调用V------每个业务都需要定义好多接口!业务一复杂,那个天呐!接口指数级增长啊!而且接口一多,就很容易丢三落四漏了调用或者调用错了。另外,这些接口其实很多都是模板代码,有经验的同学可能已经意识到了------模板代码可是一个很容易被优化的地方,比如使用注解可以让编译器自动生成很多模板代码,不必再手动编写。那MVP是否也可以免去写这些模板代码呢?MVVM背负着新时代的使命上场了!
MVVM (Model-View-ViewModel)是一种基于数据绑定 和关注点分离 的 UI 架构模式。它由微软在 2005 年(WPF 和 Silverlight 时代)正式提出,旨在通过声明式数据绑定自动化 View 与逻辑层的通信,从而解决 MVP 中接口泛滥、手动 UI 更新等痛点。
Model :与 MVC/MVP 中的 Model 一致,负责业务数据和业务逻辑。
封装数据结构和业务规则(如网络请求、数据库操作、算法计算)
不依赖任何 UI 层组件
不主动通知任何人,只提供方法供 ViewModel 调用
View :负责UI 展示和用户交互的声明式绑定
通过数据绑定(Data Binding)将 UI 控件与 ViewModel 的属性和命令连接起来
不包含任何业务逻辑或 UI 更新代码(由框架自动处理)
在 Android 中通常是 Activity、Fragment 或布局文件;在 WPF 中是 XAML;在 Vue/React 中是模板或 JSX
ViewModel(视图模型) :作为 View 的抽象,负责表现逻辑和UI 状态管理
暴露可观察属性(如 LiveData、StateFlow、ObservableField)供 View 绑定
暴露命令(如 onLoginClick)供 View 触发用户操作
调用 Model 获取业务数据,并将结果转换为 UI 可直接绑定的状态
不持有 View 的引用(无接口,无直接调用)
生命周期可独立于 View(如 Android Jetpack ViewModel 支持横竖屏重建后自动恢复)
咱们还是以那个登录的需求为例来看下MVVM是怎么实现的
// Model
class UserModel {
public boolean login(String username) {
return "admin".equals(username);
}
}
//可被观察的数据。很多平台都有封装好的,不必像这里这样自己实现。
class ObservableField<T> {
private T value;
private List<Observer<T>> observers = new ArrayList<>();
public void set(T value) {
this.value = value;
notifyObservers();
}
public T get() {
return value;
}
public void observe(Observer<T> observer) {
observers.add(observer);
}
private void notifyObservers() {
for (Observer<T> o : observers) {
o.onChanged(value);
}
}
}
interface Observer<T> {
void onChanged(T value);
}
// ViewModel
class LoginViewModel {
public ObservableField<Boolean> loading = new ObservableField<>();
public ObservableField<String> result = new ObservableField<>();
private UserModel model = new UserModel();
public void login(String username) {
loading.set(true);
boolean success = model.login(username);
loading.set(false);
if (success) {
result.set("登录成功!");
} else {
result.set("登录失败!");
}
}
}
// View
class LoginActivity {
private LoginViewModel viewModel = new LoginViewModel();
public LoginActivity() {
// 绑定 loading,对loading持续观察,一旦loading值变化了,这里就能自动触发
viewModel.loading.observe(value -> {
if (value) {
System.out.println("加载中...");
} else {
System.out.println("加载结束");
}
});
// 绑定结果,一旦结果变化了,这里就能自动触发
viewModel.result.observe(value -> {
System.out.println(value);
});
}
public void clickLogin(String username) {
viewModel.login(username);
}
}
public class Main {
public static void main(String[] args) {
LoginActivity view = new LoginActivity();
view.clickLogin("admin");
view.clickLogin("user");
}
}
上面示例中Model仍然管着业务逻辑,ViewModel中存放着可被观察的数据,View对这些数据作持续观察,ViewModel调用Model处理数据并用结果更新可观察数据,View观察到数据更新了,自动更新UI。
看下MVVM的核心特点
- 业务逻辑仍在 Model
- 状态管理在 ViewModel
- UI 更新自动响应状态变化
- View 不做业务、不操作状态
咱们来总结下MVVM相比MVP带来了哪些好处
UI 与控制逻辑彻底解耦
MVP:Presenter 仍然手动调用 View 方法控制 UI显示(命令式)
MVVM:ViewModel 只维护状态,UI 自动观察状态更新,不需要 Presenter/ViewModel 命令 UI
状态驱动 UI,减少命令式操作
MVP:每次状态变化都要在 Presenter 中显式调用 view.showXXX()
MVVM:View 订阅 ViewModel 的可观察状态,状态变化 → UI 自动刷新
优势:避免漏调用 UI 方法,降低人为错误
UI 可独立于逻辑开发和测试
MVP:UI 更新逻辑仍散布在 Presenter 方法里
MVVM:UI 只观察状态,不依赖 Presenter 调用,UI 与业务逻辑完全解耦
优势:UI 可以独立设计、测试或替换(例如不同平台的 UI)
更容易支持异步和响应式操作
MVP:异步操作后需要手动回调 Presenter 来更新 UI
MVVM:异步操作更新 ViewModel 状态,UI 自动响应
优势:简化异步状态同步,减少错误和冗余代码
归根结底:UI更新由MVP的命令式变为了状态驱动式。

双向数据绑定
在MVVM有一种较为特殊的场景------双向数据绑定。以Android的Databinding为例。当Model中数据有了变化后,Model会通知ViewModel数据变化了(例如采用View观察ViewModel中的数据,ViewModel观察Model中的数据),于是ViewModel更新数据,进而导致View更新;当View发生数据变化,例如,EditText输入了新的值,此时通知ViewModel更新数据,但是这一过程本质上还是事件(有新输入Text变化,属于UI事件)的传递。所以这种双向数据绑定是指 View < -- > ViewModel 数据自动同步,ViewModel和Model之间仍是 Model -> ViewModel 的单向数据流动。
双向数据绑定更进一步简化了模板代码,使开发变得更加简捷,但是也正是因此,排查问题变得更加困难了一些,因为这种实现方式看起来无声无息地悄悄完成,如果是平台提供的双向绑定工具还有什么特殊要求的话,在不是很清楚背后机制的情况下,可能更难以排查问题;还有业务本身可能也会在多处修改值,导致多处修改对应同一个处ViewModel数据的问题,等等不利场景,所以要视情况而定是否采用双向绑定。
深度思考
MVVM中,Model里存放着数据,VM里也存放着数据。为什么M和VM里都有数据?数据不该只存放于M里吗?
确切来说,是M里存放原始数据,VM里存放的是UI的状态,再品味一下VM的名字------视图模型,它存放的就是让UI展示的数据------UI状态,是从M里拿取原始数据后转换成的UI所需的数据。
4. MVI
在之前的几种架构中,都不可避免地出现数据和状态散落各处的情况,还可能出现某一个数据会在多处被修改的情况,修改逻辑也散落各处,状态难以追踪和管理。MVI登场了!
Model(模型) :代表应用的状态(State),是单一可信源。状态不可变,每个状态改变都会生成全新Model。
View(视图) :用户界面,负责接收状态并渲染UI,发送用户操作产生的Intent。
Intent(意图):对用户"意图"的描述,封装了所有可能改变应用状态的事件。
典型工作流程
用户触发:用户在View上点击或输入,View将其封装为Intent(如LoginIntent)并发送。
处理逻辑:Model接收Intent,执行相应业务逻辑,并计算出一个新状态(New State)。
状态更新:Model将新状态发送回View。
界面渲染:View收到新状态后重新渲染UI,完成一次闭环。
先看示例
// Model,管理状态
data class LoginState(
val username: String = "",
val password: String = "",
val isLoading: Boolean = false,
val isLoggedIn: Boolean = false,
val errorMessage: String? = null
)
//定义Intent
sealed class LoginIntent {
data class UpdateUsername(val username: String) : LoginIntent()
data class UpdatePassword(val password: String) : LoginIntent()
object SubmitLogin : LoginIntent()
object ResetError : LoginIntent()
}
// ViewModel 中处理 Intent,产出新 State
class LoginViewModel : ViewModel() {
private val _state = MutableStateFlow(LoginState())
val state: StateFlow<LoginState> = _state.asStateFlow()
// 处理Intent
fun processIntent(intent: LoginIntent) {
when (intent) {
is LoginIntent.UpdateUsername -> updateUsername(intent.username)
is LoginIntent.UpdatePassword -> updatePassword(intent.password)
LoginIntent.SubmitLogin -> submitLogin()
LoginIntent.ResetError -> resetError()
}
}
// 纯函数更新:返回新状态
private fun updateUsername(username: String) {
// 产生一个新的State值(State的内部值)并被观察者观察到
_state.update { it.copy(username = username, errorMessage = null) }
}
private fun updatePassword(password: String) {
_state.update { it.copy(password = password, errorMessage = null) }
}
private fun resetError() {
_state.update { it.copy(errorMessage = null) }
}
private fun submitLogin() {
viewModelScope.launch {
// 更新状态:开始加载,清除错误
_state.update { it.copy(isLoading = true, errorMessage = null) }
// 模拟网络请求
delay(1500)
val currentState = _state.value
val loginSuccess = (currentState.username == "admin" && currentState.password == "123")
if (loginSuccess) {
_state.update {
it.copy(
isLoading = false,
isLoggedIn = true,
errorMessage = null
)
}
} else {
_state.update {
it.copy(
isLoading = false,
isLoggedIn = false,
errorMessage = "用户名或密码错误"
)
}
}
}
}
}
@Composable
fun LoginScreen(viewModel: LoginViewModel = viewModel()) {
val state by viewModel.state.collectAsState()
// 自动处理错误消失(例如 3 秒后)
LaunchedEffect(state.errorMessage) {
if (state.errorMessage != null) {
delay(3000)
viewModel.processIntent(LoginIntent.ResetError)
}
}
Column(
modifier = Modifier.fillMaxSize().padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
OutlinedTextField(
value = state.username,
onValueChange = { viewModel.processIntent(LoginIntent.UpdateUsername(it)) },
label = { Text("用户名") },
enabled = !state.isLoading
)
Spacer(modifier = Modifier.height(8.dp))
OutlinedTextField(
value = state.password,
onValueChange = { viewModel.processIntent(LoginIntent.UpdatePassword(it)) },
label = { Text("密码") },
enabled = !state.isLoading
)
Spacer(modifier = Modifier.height(16.dp))
Button(
onClick = { viewModel.processIntent(LoginIntent.SubmitLogin) },
enabled = !state.isLoading
) {
if (state.isLoading) {
CircularProgressIndicator(modifier = Modifier.size(20.dp))
} else {
Text("登录")
}
}
if (state.errorMessage != null) {
Spacer(modifier = Modifier.height(8.dp))
Text(text = state.errorMessage!!, color = MaterialTheme.colorScheme.error)
}
if (state.isLoggedIn) {
Spacer(modifier = Modifier.height(8.dp))
Text("登录成功!", color = MaterialTheme.colorScheme.primary)
}
}
}
看完代码,大家可能就会明白了:MVI把所有事件都封装成Intent,每个事件都对应着一个Intent;把UI的所有状态都封装成一个State------UI所需的所有状态都封装进一个State对象里。所有的Intent都交由同一个Reducer(即Intent消费者,或者叫处理者)来串行处理,执行完成后生成新的State;而View则持续观察State,一旦State有更新就刷新UI。MVI把UI状态的管理集中到了Reducer里,使状态的维护、追踪、调试都变得清晰。
MVI架构相对于之前MVC -> MVP -> MVVM 的架构演变,变化相对较小。我目前还没有使用过这种架构,理解还不够深刻。
深度思考
1、所有Intent都交由一个Reducer串行处理,它岂不会成为瓶颈?而且它岂不会越来越臃肿?
Reducer真正要做的其实产生新的State值,它主要做的任务是对象拷贝和字段修改,所以并不耗时。对于耗时的任务常用的策略,以加载数据为例,当 Reducer 收到一个 Intent(如LoadDataIntent)时,它会立即返回一个 加载中 的新 State(isLoading = true)。
同时,ViewModel 会启动一个协程执行异步操作,操作完成后,再发送一个新的 Intent(如 DataLoadedIntent)给 Reducer,Reducer 再次同步生成包含数据的最终 State。这样既保证了串行状态更新,又不会阻塞异步操作,也不会让 Reducer 成为瓶颈。 因为 Reducer 本身只做轻量级的状态转换,真正的耗时操作都在外部处理。
对于Reducer会随着Intent的增加变得臃肿,确实有可能,但也是可以优化的。将大的 State 拆分成多个子 State,每个子 State 有自己的 Reducer,然后在主 Reducer 中组合。如下
data class LoginState(
val form: FormState,
val submitStatus: SubmitStatus
)
data class FormState(val username: String, val password: String)
data class SubmitStatus(val isLoading: Boolean, val error: String?)
// 子 Reducer
fun formReducer(state: FormState, intent: FormIntent): FormState { ... }
fun submitReducer(state: SubmitStatus, intent: SubmitIntent): SubmitStatus { ... }
// 主 Reducer
fun mainReducer(state: LoginState, intent: Intent): LoginState {
return when (intent) {
is FormIntent -> state.copy(form = formReducer(state.form, intent))
is SubmitIntent -> state.copy(submitStatus = submitReducer(state.submitStatus, intent))
}
}
但是这样又在一定程度上把对State的管理打散了!所以,这确实是一个需要权衡的问题。还有一些其他优化措施,属于实践的部分,这里不再过多陈述。
2、每次State更新都要刷新UI,如果State更新太频繁,岂不要不停刷新UI?
确实会这样,所以MVI更适合搭配Compose这种有差异化比较机制的UI框架,因为这种UI框架会比较本次和上次UI的差异,最后只刷新有差异的部分,而对于Android传统视图,MVI确实相对麻烦一点,有可能要做一些优化措施,否则仅仅是为了更新一处文字,也可能会大量UI组件重新绘制;可以采取多种措施优化,但是实施起来的麻烦程度可能会得不偿失。
5. Android开发架构演变史
Android刚刚兴起时,由于业务简单,官方的文档和示例也是MVC架构,所以MVC非常适配当时的场景,开发起来简单、高效、直观;之后随着业务增长,MVC开始暴露出了它的缺点,之前在其他平台发生的事也开始出现在Android平台上,于是MVP开始替代MVC,避免VC耦合、代码臃肿、难以维护、难以单元测试这些问题;但是MVP确实需要太多接口了,实现起来很麻烦,此时Android发展也有些年头了,也开始注重各种优化,官方也开始推出对MVVM的支持(jetpack),大大方便了对复杂业务开发的支持。但是并不是越后来出现的架构就更优秀,还是要根据具体场景来决定采用哪种架构。MVI作为最后出现的一种架构,我目前还没有实践过,以后再补充吧。
MVC: 适合交互简单、业务简单的场合,不需要那么多架构思考,快速开发,结构明了。
MVP: 更适合界面交互较为复杂的应用,特别是在需要大量测试和维护的项目中,MVP对测试最友好。但是如今由于Jetpack的诞生,MVVM搭配Jetpack实在是一种更好的开发选择,再加上MVP需要定义许许多多的接口......所以我觉得目前MVVM会是一个比MVP更好的选择。我建议可以略过MVP。
MVVM: 适合中到大型的应用,尤其是在需要数据绑定和响应式UI的场景下表现最好,结合LiveData和ViewModel使用时非常高效。
**MVI:**适合非常复杂的UI状态管理,或者需要非常清晰、可预测的数据流和状态管理的大型应用。
MVI的出现似乎并不是专门为了解决包括MVVM在内的数据、状态修改较分散的问题,它更像是一种全新理念的尝试。