对于一个应用,内部可以有多个架构模式并存,而不是一定要所有的页面都用MVVM或者MVP或者MVI,而应该根据业务需要,合适的选择适合的架构来做调整,甚至都可以不用这些架构。
-
模式 vs. 框架
- MVC、MVP、MVVM -- 都是 UI 架构模式,用来规定 层次职责与依赖方向。
- DataBinding、Jetpack Compose、Vue 等只是实现 MVVM 思想的 框架/库。
-
DataBinding ≠ 自动 MVVM
- 判定是否 MVVM,看 View 是否只负责声明 UI、事件上报,业务状态和逻辑是否完全落在 ViewModel。
- 仅使用 DataBinding 若 View 仍直接操作 Model,就不算 MVVM。
-
“MVVM = MVP + 双向绑定” 不严谨
-
两者都解耦 UI 与业务,但
- MVP:Presenter 主动 push 数据给 View;
- MVVM:View 订阅 ViewModel 暴露的状态(可单向或双向)。
-
绑定机制只是 MVVM 常见手段,不是唯一特征。
-
-
Google ViewModel 只是实现之一
androidx.lifecycle.ViewModel满足 MVVM 中“状态存储 + 生命周期感知”的角色,但 MVVM 并不依赖该库;任何平台都可自定义 ViewModel。
-
总结
- 模式:MVC / MVP / MVVM
- 实现工具:DataBinding / Compose / Vue …
- 核心衡量:职责分层,而非是否用了某个绑定库。
目前,很多 Android 开发者对 MVC 存在误解,认为在 Activity 中既写业务逻辑又进行界面更新就是 MVC,其实这并不是严格意义上的 MVC 架构。
首先来简单介绍下三种不同的模式:
MVC
MVC的整体架构如下:
一个简单,但是却能清晰的表明MVC页面架构的demo:
// ---------------------
// View
// ---------------------
class View {
// Holds the Controller so user actions can be forwarded.
val controller: Controller = /* ... */
// Displays the data to the user (e.g., printing to console or updating UI).
fun showData(data: Int) {
// ...
}
// Called when the user performs some action (e.g., clicks a button).
fun clicked() {
controller.onViewClicked()
}
}
// ---------------------
// Controller
// ---------------------
class Controller {
// Holds the Model so it can trigger data updates.
val model: Model = /* ... */
// Invoked by the View to handle user input. Tells the Model to update.
fun onViewClicked() {
model.processData()
}
}
// ---------------------
// Model
// ---------------------
class Model {
// Holds the View so it can notify it of changes.
val view: View = /* ... */
// Core data.
var data = 0
// Processes or updates the data, then informs the View.
fun processData() {
data++
view.showData(data)
}
}
-
角色职责明确且精简
- Model:仅包含核心数据
data以及更新数据的逻辑processData();更新后立即调用view.showData()来通知视图刷新。 - View:只负责显示数据(
showData())和响应用户操作(clicked()),并把用户操作转发给 Controller。 - Controller:仅在收到 View 的用户操作回调(
onViewClicked())后,通知 Model 更新数据。
这样的职责划分简单清晰,没有额外的业务逻辑或数据处理流程。
- Model:仅包含核心数据
-
数据流与调用链路简单
- 用户操作(View)→
clicked()→ (通知) → Controller - Controller →
onViewClicked()→ (更新) → Model - Model →
processData()→ (更新数据并通知) → View
数据流在这三者之间形成一个最基本的环路,整个调用过程易于理解和追踪。
- 用户操作(View)→
不过一般的,我们在Android中写代码的时候,一般不会将View和Model直接写在一起。所以我们平常所说的把所有业务逻辑和界面更新操作都写进 Activity 的写法并不是标准的MVC,这种模式应该叫做M-VC。
在安卓页面中,MVC的分工:
M:Model 单独文件/package
V: View XML
C: Controller Activity/Fragment
由于android中xml布局的功能性太弱,Activity实际上负责了View层与Controller层两者的工作,所以在android中mvc更像是这种形式:
标准的MVC结构具备了:
优点:结构清晰、简单,分工明确,
缺点:模块之间存在耦合;特别的,在安卓中Activity同时负责View与Controller层的工作,违背了单一职责原则。
应用场景:设置/历史订单/交易记录/收藏,因为这些场景的逻辑简单。
MVP
由于MVC架构在Android平台上的一些缺陷,因此MVP架构应运而生,MVP通过代码分层将Activity中的各项职能隔离开来,以接口回调的形式进行数据通信。
MVP的整体架构如下:

M: Model
V: View: xml + activity + view的interface
P: Presenter 逻辑 view和model的交互
View层:对应于Activity与XML,只负责显示UI,只与Presenter层交互,与Model层没有耦合;Presenter层: 主要负责处理业务逻辑,通过接口回调View层;Model层:主要负责网络请求,数据库处理等操作,这个没有什么变化;
优点:MVP解决了MVC的两个问题,即Activity承担了两层职责与View层与Model层耦合的问题
缺点:
(1) Presenter层通过接口与View通信,实际上持有了View的引用;
(2) 但是随着业务逻辑的增加,一个页面可能会非常复杂,这样就会造成View的接口会很庞大。
下边是一个典型的 MVP(Passive View)实现,完全符合「Presenter 持有 Model 和 View、Model 不依赖 Presenter、View 只负责展示」的三层分离原则:
-
Model
- 纯业务逻辑(
Board),不引用也不知道有 Presenter 的存在。 - 只提供同步的
mark()、getWinner()、restart()等方法。
- 纯业务逻辑(
-
Presenter
- 持有对 Model 的引用,用来驱动业务;
- 持有对 View(
MyView接口)的引用,用来回调更新 UI; - 唯一入口 是
onButtonSelected()和onResetSelected(),串起 M→P→V。
-
View
MyActivity实现了MyView,只负责把用户点击“翻译”成 row/col,然后把 Presenter 的回调结果渲染到界面上。- 不包含任何业务逻辑,也不直接操作 Model。
这一结构正是 Passive View 变体(View 最“傻”,全由 Presenter 驱动),也常被称为「Supervising Controller」——它是 Android 上最常见的 MVP 形式。
示例代码:
MyActivity.java - View 层:负责用户交互和展示,只调用 Presenter
package com.example.mvx.view;
import android.os.Bundle;
import android.util.Log;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.TextView;
import androidx.appcompat.app.AppCompatActivity;
import com.example.mvx.R;
import com.example.mvx.presenter.MyPresenter;
/**
* MyActivity:MVP 架构中的 View 层
* - 负责初始化和渲染 UI
* - 将用户操作(点击/菜单)转发给 Presenter
* - 通过 MyView 接口的回调方法,接收 Presenter 下发的更新指令
*/
public class MyActivity extends AppCompatActivity {
private static final String TAG = MyActivity.class.getName();
// —— UI 组件引用 ——
private ViewGroup buttonGrid; // 存放棋盘按钮的容器(3x3 Grid)
private View winnerPlayerViewGroup; // 显示赢家信息的布局
private TextView winnerPlayerLabel; // 显示赢家名称的文本
// —— Presenter 引用 ——
// 使用匿名内部类实现 MyView,把 View 的更新逻辑传给 Presenter
private MyPresenter presenter = new MyPresenter(new MyView() {
// 下边这些都是presenter层通知view该做什么事情,也就是presenter->view的通信。
@Override
public void setButtonText(int row, int col, String text) {
// 根据 row/col 拼接 tag,找到对应的按钮并设置文字
Button btn = (Button) buttonGrid.findViewWithTag("" + row + col);
if (btn != null) {
btn.setText(text);
}
}
@Override
public void clearButtons() {
// 遍历所有子 View(按钮),依次清空文本
for (int i = 0; i < buttonGrid.getChildCount(); i++) {
((Button) buttonGrid.getChildAt(i)).setText("");
}
}
@Override
public void showWinner(String winningPlayerDisplayLabel) {
// 设置赢家名称,并将对应布局显示出来
winnerPlayerLabel.setText(winningPlayerDisplayLabel);
winnerPlayerViewGroup.setVisibility(View.VISIBLE);
}
@Override
public void clearWinnerDisplay() {
// 隐藏赢家信息布局,并清空文本
winnerPlayerViewGroup.setVisibility(View.GONE);
winnerPlayerLabel.setText("");
}
});
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// 加载布局文件 jingziqi.xml
setContentView(R.layout.jingziqi);
// 获取并缓存各个控件实例
winnerPlayerLabel = findViewById(R.id.winnerPlayerLabel);
winnerPlayerViewGroup = findViewById(R.id.winnerPlayerViewGroup);
buttonGrid = findViewById(R.id.buttonGrid);
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
// 加载菜单资源 menu_jingziqi.xml
MenuInflater inflater = getMenuInflater();
inflater.inflate(R.menu.menu_jingziqi, menu);
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
// 处理菜单项点击
switch (item.getItemId()) {
case R.id.action_reset:
// 用户点击“重置”时,通知 Presenter 重置游戏状态
presenter.onResetSelected();
return true;
default:
return super.onOptionsItemSelected(item);
}
}
/**
* View->Presenter 层的通信
* 布局中所有棋格按钮的 onClick 回调方法
* 拿到被点击按钮的 tag,解析出 row 和 col,然后交给 Presenter 处理
*/
public void onCellClicked(View v) {
Button button = (Button) v;
String tag = button.getTag().toString(); // tag 形如 "00","01"…
int row = Integer.parseInt(tag.substring(0, 1)); // 取行号
int col = Integer.parseInt(tag.substring(1, 2)); // 取列号
Log.i(TAG, "Click Row: [" + row + "," + col + "]");
// 将点击事件转发给 Presenter,触发业务逻辑
presenter.onButtonSelected(row, col);
}
}
MyPresenter.java - Presenter 层:调用 Model,处理逻辑,然后通过 View 接口更新 UI
package com.example.mvx.presenter;
import com.example.mvx.model.Board;
import com.example.mvx.model.Player;
import com.example.mvx.view.MyView;
public class MyPresenter {
private MyView view; // 引用 View 接口
private Board model; // 引用 Model
/**
* 构造:注入 View,并新建一个 Model 实例
*/
public MyPresenter(MyView view) {
this.view = view;
this.model = new Board();
}
/**
* 用户点击格子事件:
* 1. 调用 Model.mark()
* 2. 如果成功,更新 View
* 3. 检查胜利,调用 View.showWinner()
*/
public void onButtonSelected(int row, int col) {
// 1. Presenter<->Model通信,让 Model 落子,触发Model的逻辑
Player playerThatMoved = model.mark(row, col);
if (playerThatMoved != null) {
// 2. 回调 View 更新对应按钮文字,
// 获取View的状态后,更新View,这也是为什么Presenter必须要持有View的引用。
view.setButtonText(row, col, playerThatMoved.toString());
// 3. 如果有胜利者,再回调 View 显示胜利
if (model.getWinner() != null) {
view.showWinner(playerThatMoved.toString());
}
}
}
/**
* 用户点击“重置”按钮:
* 1. 重置 Model
* 2. 通知 View 清除 UI
*/
public void onResetSelected() {
model.restart(); // 重置棋盘数据
view.clearWinnerDisplay(); // 隐藏胜利提示
view.clearButtons(); // 清空所有按钮
}
}
Board.java - Model 层,纯粹的数据层,只负责提供数据,以及一些简单的数据处理。不能持有Presenter以及View。重逻辑需要放入 Presenter,维护游戏状态(棋盘、回合、胜利检测)。
package com.example.mvx.model;
import static com.example.mvx.model.Player.O;
import static com.example.mvx.model.Player.X;
public class Board {
private Cell[][] cells = new Cell[3][3];
private Player winner;
private GameState state;
private Player currentTurn;
public Board() {
restart(); // 初始化或重置时调用
}
/**
* 重置棋盘:清空格子,状态设为 IN_PROGRESS,当前回合设为 X
*/
public void restart() {
clearCells();
winner = null;
currentTurn = X;
state = GameState.IN_PROGRESS;
}
/**
* 落子方法:
* - 如果游戏结束或该格子已被占,则忽略
* - 否则标记当前玩家,并检查胜利/切换回合
* @return 本次落子的玩家,或 null(无效)
*/
public Player mark(int row, int col) {
if (!isValid(row, col)) return null;
cells[row][col].setValue(currentTurn);
Player moved = currentTurn;
// 检查是否形成三连
if (isWinningMoveByPlayer(currentTurn, row, col)) {
state = GameState.FINISHED;
winner = currentTurn;
} else {
// 切换玩家回合
flipCurrentTurn();
}
return moved;
}
/**
* 获取胜利者,若无赢家则返回 null
*/
public Player getWinner() {
return winner;
}
// 辅助方法:清空所有 Cell 对象
private void clearCells() {
for (int i = 0; i < 3; i++) {
for (int j = 0; j < 3; j++) {
cells[i][j] = new Cell();
}
}
}
// 验证落子是否合法:游戏未结束且该格为空
private boolean isValid(int row, int col) {
return state != GameState.FINISHED && cells[row][col].getValue() == null;
}
// 判断三连胜利条件
private boolean isWinningMoveByPlayer(Player player, int r, int c) {
return (cells[r][0].getValue() == player && cells[r][1].getValue() == player && cells[r][2].getValue() == player)
|| (cells[0][c].getValue() == player && cells[1][c].getValue() == player && cells[2][c].getValue() == player)
|| (r == c && cells[0][0].getValue() == player && cells[1][1].getValue() == player && cells[2][2].getValue() == player)
|| (r + c == 2 && cells[0][2].getValue() == player && cells[1][1].getValue() == player && cells[2][0].getValue() == player);
}
// 回合切换:X ↔ O
private void flipCurrentTurn() {
currentTurn = (currentTurn == X ? O : X);
}
}


持有的关系:Presenter持有了View的接口,同时Presenter层内部还持有了Model。通过这个接口,来实现视图的更新。为什么增加接口,而不是让Presenter层直接持有View的引用? 这是因为 Presenter 不应该依赖具体的 Activity/Fragment(Android 组件),而是依赖一个“抽象” View 接口。
测试源码链接:github.com/xingchaozha…
MVVM
MVVM 模式将 Presenter 改名为 ViewModel,基本上与 MVP 模式完全一致。 双向数据绑定。相对于MVP,我们去掉了view的interface。现在有个问题,既然我们去掉了interface,但是我们的视图还是xml和Activity的,那怎么来响应数据的变化呢?对于此,谷歌采用了databinding来让我们更方便的实现数据与视图的双向绑定,当然,如果你不想使用databinding,也可以自己去实现数据与视图的双向绑定,因为Databinding只是将我们这种双向绑定的过程帮我们实现了。这就需要在xml中加入一些代码。就是databinding生成的xml。View的变动,自动反映在 ViewModel,反之亦然。
MVVM架构图如下所示:
View观察ViewModel的数据变化并自我更新,这其实是单一数据源而不是双向数据绑定,所以其实MVVM的这一大特性很多时候并没有用到;View通过调用ViewModel提供的方法来与ViewMdoel交互。

进一步
即便不使用Databinding,我们也能实现数据与视图的双向绑定,因为MVVM的核心就是数据驱动UI,UI变化更新数据, 什么是数据绑定:
- 外部数据(数据库数据、⽹络数据)、内存数据(Java 代码中的变量)、表现数据(界⾯中展示的数据)中的外部表现数据和内存数据互相⾃动更新。
- 另外,MVVM 有时候还可以给我们的内存数据和数据库数据做关联监听,让我们的这三种数据实现进⼀步的联动。
例如下边的例子:
// MvvmActivity.kt — View 层:启动 ViewModel 并绑定视图
class MvvmActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// 创建 ViewModel,传入需要绑定的 EditText
ViewModel(data1View, data2View).init()
}
}
// StringAttr.kt — Model 层:数据载体,携带变更监听接口
class StringAttr {
// 持有实际数据
var value: String? = null
set(value) {
field = value
// 内存数据改变时,通知注册的监听器(ViewModel/绑定器)
onChangeListener?.onChange(value)
}
// 变更回调接口,可由 ViewBinder 注册
var onChangeListener: OnChangeListener? = null
interface OnChangeListener {
fun onChange(newValue: String?)
}
}
// ViewModel.kt — ViewModel 层:持有 UI 对应的 StringAttr,并负责初始化数据
class ViewModel(
private val data1View: EditText,
private val data2View: EditText
) {
// 两个属性,双向绑定到对应的 EditText
var data1: StringAttr = StringAttr()
var data2: StringAttr = StringAttr()
init {
// 绑定 View 和 Model,通过 ViewBinder 建立双向同步
ViewBinder.bind(data1View, data1)
ViewBinder.bind(data2View, data2)
}
/**
* 初始化:从数据源获取数据,并更新到 StringAttr,进而自动更新视图
*/
fun init() {
val data = DataCenter.getData() // 假设从网络或数据库取回 List<String>
data1.value = data[0] // 赋值后,触发 onChangeListener -> 更新 data1View
data2.value = data[1]
}
}
// ViewBinder.kt — 绑定器:模拟 DataBinding,实现 EditText 与 StringAttr 的双向同步
class ViewBinder {
companion object {
/**
* 将 EditText 与 StringAttr 绑定:
* - 用户输入时,更新 StringAttr.value
* - StringAttr.value 改变时,回写到 EditText.text
*/
fun bind(editText: EditText, stringAttr: StringAttr) {
// 1. 监听用户输入,更新内存数据
editText.doAfterTextChanged { editable ->
val newText = editable?.toString()
if (!TextUtils.equals(stringAttr.value, newText)) {
// 避免死循环:只有值不同时才写入
stringAttr.value = newText
println("[View → Model] 用户输入,更新内存数据: $newText")
}
}
// 2. 监听内存数据变化,更新 UI
stringAttr.onChangeListener = object : StringAttr.OnChangeListener {
override fun onChange(newValue: String?) {
if (!TextUtils.equals(newValue, editText.text)) {
// 避免死循环:只有值不同时才设置
editText.setText(newValue)
println("[Model → View] 内存通知视图更新: $newValue")
}
}
}
}
}
}
上边的例子,我们即便没有使用Databinding,也可以实现数据驱动UI。
-
Model 层:
- 例如
StringAttr类,仅用于保存和管理数据。它通过onChangeListener通知数据变化,这部分只关注数据状态,不涉及 UI 展示。
- 例如
-
View 层:
- 例如
MvvmActivity以及布局中的EditText,只负责展示界面和接收用户输入,不包含业务逻辑。
- 例如
-
ViewModel 层:
ViewModel类作为中介,负责将 Model 和 View 关联起来。它使用ViewBinder实现双向绑定,将 UI 控件(EditText)的变化同步到数据模型(StringAttr),反过来数据变化也自动更新到 UI。这种数据绑定使得业务逻辑与界面展示彻底解耦,符合 MVVM 的核心思想。
-
双向数据绑定:
ViewBinder.bind()方法通过监听 EditText 的文本变化和 StringAttr 的变化,实现了双向数据同步。这样,当用户修改输入时,数据模型会更新;而当数据模型发生变化时,UI 也会自动刷新,完全符合 MVVM 模式中“视图绑定”的特性。
因此,这个代码在结构上实现了 MVVM 模式:
- Model 只处理数据;
- View 只负责展示;
- ViewModel 负责业务逻辑、状态管理和数据绑定,使得 UI 与数据保持同步。
实现数据驱动是ViewModel,为什么叫ViewModel?因为其实这个就是View的数据。一般的,我们会使用LiveData,一种能够感知生命周期的数据,目前谷歌正在大力推广Compose,新的UI库,导致该方式与DataBinding已经逐渐的过时了。
M: Model
V: xml + activity
VM: ViewModel
优点:角色清晰
缺点:View与ViewModel通过ViewModel暴露的方法交互,比较零乱难以维护
应用场景:首页/复杂需求多变的页面
小结
-
MVC架构的主要问题在于Activity承担了View与Controller两层的职责,同时View层与Model层存在耦合 -
MVP引入Presenter层解决了MVC架构的两个问题,View只能与Presenter层交互,业务逻辑放在Presenter层 -
MVP的问题在于随着业务逻辑的增加,View的接口会很庞大,MVVM架构通过双向数据绑定可以解决这个问题 -
MVVM与MVP的主要区别在于,你不用去主动去刷新UI了,只要Model数据变了,会自动反映到UI上。换句话说,MVVM更像是自动化的MVP。 -
MVVM的双向数据绑定主要通过DataBinding实现,但有很多人(比如我)不喜欢用DataBinding,而是View通过LiveData等观察ViewModle的数据变化并自我更新,这其实是单一数据源而不是双向数据绑定
MVI
在讨论MVI之前,我们先看MVVM有什么不足:
为了保证数据流的单向流动,对外暴露的LiveData是只读的,这样外部观察者(如Activity或Fragment)只能订阅数据变化,而不能直接修改状态。这可以防止外部意外地改变状态,确保数据的一致性和安全性。LiveData向外暴露时需要转化成immutable的,这需要添加不少模板代码并且容易遗忘:
class TestViewModel : ViewModel() {
// 1. 私有 MutableLiveData:只在 ViewModel 内部可写,用于存放并更新 ExampleState
private val _exampleState: MutableLiveData<ExampleState> = MutableLiveData()
// 2. 对外暴露 LiveData:UI 层只能订阅 exampleState,无法修改,保证数据流单向,因为LiveData不支持修改数据。
val exampleState: LiveData<ExampleState> = _exampleState
// ====== 更多字段示例 ======
// 私有 MutableLiveData:内部更新 state1 字段的值
private val _state1: MutableLiveData<String> = MutableLiveData()
// 对外只读 LiveData:UI 层订阅后即可根据 state1 值刷新界面
val state1: LiveData<String> = _state1
// 私有 MutableLiveData:内部更新 state2 字段的值
private val _state2: MutableLiveData<String> = MutableLiveData()
// 对外只读 LiveData:防止外部误写 state2,保持状态封装性
val state2: LiveData<String> = _state2
// ... 如果有更多状态字段,按照同样模式定义即可
/**
* 示例:更新 ExampleState 的方法
*/
fun updateExample(newState: ExampleState) {
// 只在 ViewModel 内部调用,触发 UI 层的订阅者接收新状态
_exampleState.value = newState
}
/**
* 示例:更新 state1 和 state2 的方法
*/
fun updateStates(value1: String, value2: String) {
_state1.value = value1
_state2.value = value2
}
}
如上所示,如果页面逻辑比较复杂,ViewModel中将会有许多全局变量的LiveData,并且每个LiveData都必须定义两遍,一个可变的,一个不可变的。这其实就是我通过MVVM架构写比较复杂页面时最难受的点。
其次就是View层通过调用ViewModel层的方法来交互的,View层与ViewModel的交互比较分散,不成体系
小结一下,在使用中,MVVM架构主要有以下不足:
- 为保证对外暴露的
LiveData是不可变的,需要添加不少模板代码并且容易遗忘 View层与ViewModel层的交互比较分散零乱,不成体系
MVI 与 MVVM 很相似,其借鉴了前端框架的思想,更加强调数据的单向流动和唯一数据源,它通过引入不可变状态和统一的Action来确保数据只单向流动。因为我们很多时候并不需要实现UI更新后同步更新数据的逻辑。
其主要分为以下几部分
Model: 与MVVM中的Model不同的是,MVI的Model主要指UI状态(State)。例如页面加载状态、控件位置等都是一种UI状态,接收 Intent,处理业务逻辑,并生成一个全新的、不可变的状态。View: 与其他MVX中的View一致,可能是一个Activity或者任意UI承载单元。MVI中的View通过订阅Model的变化,并根据新的状态重新渲染 UI。Intent: 此Intent不是Activity的Intent,用户的任何操作都被包装成Intent后发送给Model层进行数据请求,捕捉用户的各种操作(如点击、滑动等),将其转换为明确的用户意图。- 循环数据流:用户在 View 上的交互再次产生新的 Intent,形成单向循环数据流。
MVI强调数据的单向流动,主要分为以下几步:
- 用户操作以
Intent的形式通知Model Model基于Intent更新StateView接收到State变化刷新UI。
数据永远在一个环形结构中单向流动,不能反向流动:
下面提供一个简单的示例,展示如何使用 ViewModel 来实现 MVI 架构,其中:
- ViewState 是一个 data class,包含了所有页面的状态(例如计数、加载状态、错误信息等)。
- Action 使用 sealed class 表示用户在 View 层的行为。
- ViewModel 作为 Model 层,内部维护了一个 MutableLiveData 来保存当前的 ViewState,并对外只暴露不可变的 LiveData。View 层发送 Action 给 ViewModel,ViewModel 根据不同的 Action 更新状态,然后 View 层通过观察状态变化来刷新 UI。
示例代码
// 定义 ViewState:包含所有页面状态的 data class,也是数据模型,Model
data class MyViewState(
val count: Int = 0, // 计数器状态
val isLoading: Boolean = false, // 加载状态
val error: String? = null // 错误信息
)
// 定义 Action:描述用户行为的 sealed class
sealed class MyAction {
object Increment : MyAction() // 增加计数
object Decrement : MyAction() // 减少计数
object LoadData : MyAction() // 加载数据
}
// 使用 ViewModel 承载 MVI 中的 Model 层,
// ViewModel 本身只是把「Model(状态)」「Intent(操作)」「View(订阅者)」三者串起来的桥梁。
class MyViewModel : ViewModel() {
// 内部可变的 ViewState,仅供 ViewModel 内部更新
private val _viewState = MutableLiveData<MyViewState>(MyViewState())
// 对外只暴露不可变的 LiveData,View 只能订阅状态变化
val viewState: LiveData<MyViewState> = _viewState
// 处理来自 View 的 Action,并更新状态
fun processAction(action: MyAction) {
when (action) {
is MyAction.Increment -> {
val currentState = _viewState.value ?: MyViewState()
_viewState.value = currentState.copy(count = currentState.count + 1)
}
is MyAction.Decrement -> {
val currentState = _viewState.value ?: MyViewState()
_viewState.value = currentState.copy(count = currentState.count - 1)
}
is MyAction.LoadData -> {
// 更新状态为加载中
val currentState = _viewState.value ?: MyViewState()
_viewState.value = currentState.copy(isLoading = true)
// 模拟加载数据(实际开发中可能是异步操作),加载完成后更新状态
// 例如:
// _viewState.value = currentState.copy(isLoading = false, count = newCount)
}
}
}
}
在 Activity 或 Fragment 中使用
class MyActivity : AppCompatActivity() {
private lateinit var viewModel: MyViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// 初始化 ViewModel
viewModel = ViewModelProvider(this).get(MyViewModel::class.java)
// 订阅 ViewState 变化,根据状态更新 UI
viewModel.viewState.observe(this, Observer { state ->
// 根据 state 渲染 UI,例如:
textViewCount.text = state.count.toString()
progressBar.visibility = if (state.isLoading) View.VISIBLE else View.GONE
// 如 state.error 不为空,可以显示错误信息
})
// 用户交互:通过发送 Action 更新状态
buttonIncrement.setOnClickListener {
viewModel.processAction(MyAction.Increment)
}
buttonDecrement.setOnClickListener {
viewModel.processAction(MyAction.Decrement)
}
buttonLoadData.setOnClickListener {
viewModel.processAction(MyAction.LoadData)
}
}
}
示例说明
- ViewState(状态层)
定义了页面所有需要展示的数据状态,采用不可变的 data class,每次更新都会生成新的状态对象。 - Action(意图层)
通过 sealed class 定义所有可能的用户操作,这样可以确保每个 Action 都被穷举,便于维护和扩展。 - ViewModel(Model 层)
内部使用 MutableLiveData 来保存 ViewState,并只对外暴露 LiveData;通过processAction()方法根据用户的 Action 来更新状态。这样,状态更新完全由 ViewModel 控制,View 层只需要观察状态变化即可。 - View 层
负责展示 UI,并将用户的操作转化为 Action 发送给 ViewModel,而不直接调用 ViewModel 中的方法来修改数据。这种方式能更好地实现单向数据流,使状态管理更明确。
这个示例展示了如何在 MVI 架构下使用 ViewModel 来承载 Model 层,通过 ViewState 和 Action 实现状态管理和数据更新,View通过Action与ViewModel交互,通过 Action 通信,有于 View 与 ViewModel 之间的进一步解耦,同时所有调用以 Action 的形式汇总到一处,也有利于对行为的集中分析和监控。
在 MVI 里,M 确实是指 “Model”,但它跟我们在领域层(Domain)里说的 “Model” 并不完全一样——它就是专门用来描述UI 当前状态(State)的那份 “模型”。
-
我们把它叫做
ViewState或UiState,只是为了强调它是 UI 的「状态快照」,而不是业务层的实体(比如User、Order、Product)。 -
本质上,这个
ViewState就是 MVI 架构中的 Model:- 它是 单一可信源(Single Source of Truth),
- 它保存并描述了所有需要在界面上渲染的数据,
- 每次发生变化时都会生成一个新的不可变对象。
之所以不直接叫 Model 而用 ViewState/UiState,主要是为了和其他层面的“Model”区分开来,让人一看就知道这是 MVI 里的那份“UI 模型/状态”,而不是后端数据模型。
换句话说:
MVI 的 “M” = “ViewState”
I = “Intent”(用户意图/事件)
V = “View”(渲染层)
所以,把它命名为 MyViewState 并不会违背 MVI,只是让代码语义更清晰、更自解释。
小结MVC,MVP,MVVM与MVI架构
目前MVVM是官方推荐的架构,但仍然有以下几个痛点:
MVVM与MVP的主要区别在于双向数据绑定,但由于很多人不喜欢使用DataBindg,其实并没有使用MVVM双向绑定的特性,而是单一数据源- 当页面复杂时,需要定义很多
State,并且需要定义可变与不可变两种,状态会以双倍的速度膨胀,模板代码较多且容易遗忘 View与ViewModel通过ViewModel暴露的方法交互,比较零乱难以维护
而MVI可以比较好的解决以上痛点,它主要有以下优势
- 强调数据单向流动,很容易对状态变化进行跟踪和回溯
- 使用
ViewState对State集中管理,只需要订阅一个ViewState便可获取页面的所有状态,相对MVVM减少了不少模板代码 ViewModel通过ViewState与Action通信,通过浏览ViewState和Aciton定义就可以理清ViewModel的职责,可以直接拿来作为接口文档使用。
当然MVI也有一些缺点,比如
- 所有的操作最终都会转换成
State,所以当复杂页面的State容易膨胀 state是不变的,因此每当state需要更新时都要创建新对象替代老对象,这会带来一定内存开销
MVI 的精髓就在于把UI 完全当作“State 的函数”来写,这不就是声明式 UI 的雏形吗?你只要关心「给我一个状态,如何渲染界面」,而不必在各处去手写“显示/隐藏”“添加/移除”控件的命令式逻辑。
Compose
从MVC到MVP,我们让Activity的职责变得单一,从MVP到MVVM或者是MVI,我们干掉了view的interface,但是还有xml,于是我们想,能不能把xml也干掉?让Activity只专注于视图展示?
使用Compose的方式就可以完全规避掉这些问题了。
天生就是数据驱动 ,不需要使用viewbinding与DataBinding,导致fragment/livedata/databinding/viewbinding/view/viewgroup的过时。
M: Model
V: activity + composable
VM: ViewModel, 数据类型变了 livedata ->state的数据,如果数据想要自动更新,就需要使用MutableState 为什么说Compose自带数据驱动?因为Compose采用了声明式UI,传统的View采用了命令式布局,需要动态的为每一个View手动设置值。
使用下边的声明式的View来确定View的状态:
fun MyMainView(viewModel:MyViewModel) {
Column(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.Top,
horizontalAlignment = Alignment.Companion.CenterHorizontally
) {
LazyVerticalGrid(
cells = GridCells.Fixed(3)
) {
items(9) {
TextButton(
enabled = true,
onClick = { viewModel.onClickedCellAt(it / 3, it % 3) },
modifier = Modifier
.padding(20.dp)
.fillMaxWidth()
.height(88.dp)
.background(
Color.Gray
)
.testTag(it.toString())
) {
Text(viewModel.cellList[it].value ?: "", fontSize = 25.sp, color = Color.White)
}
}
}
Text(text = viewModel.winner.value, modifier = Modifier.padding(10.dp), fontSize = 25.sp)
if (viewModel.winner.value.isNotEmpty()) {
Text(text = "赢了", fontSize = 25.sp)
}
}
}
Compose的ViewModel:必须要使用下边几种类型,才能正常的关注数据状态的变化,但是一般的,MutableState更通用。
一、Compose 能自动跟踪的状态类型
-
SnapshotState(
mutableStateOf/mutableStateListOf等)var count by mutableStateOf(0) val list = mutableStateListOf<String>() -
LiveData
// ViewModel private val _foo = MutableLiveData<String>() val foo: LiveData<String> = _foo // Compose 中 val fooValue by viewModel.foo.observeAsState("") -
StateFlow / Flow
// ViewModel private val _bar = MutableStateFlow(0) val bar: StateFlow<Int> = _bar // Compose 中 val barValue by viewModel.bar.collectAsState() -
SavedStateHandle
- 结合
savedStateHandle.getStateFlow(...),同样可以在 Compose 中用collectAsState()。
class MyViewModel : ViewModel() { private val model: Board = Board() val winner = mutableStateOf("") val cellList: List<MutableState<String?>> = ArrayList() fun onResetSelected() { model.restart() for (i in 0..8) { cellList[i].value = "" } winner.value = "" } fun onClickedCellAt(row: Int, col: Int) { val playerThatMoved = model.mark(row, col) if (playerThatMoved != null) { winner.value = (if (model.winner == null) "" else model.winner.toString()) cellList[row * 3 + col].value = if (playerThatMoved == null) null else playerThatMoved.toString() } } init { for (i in 0..8) { (cellList as ArrayList).add(mutableStateOf("")) } } } - 结合
MutableState是一种特殊的状态容器,用于持有UI状态的值。当这个值改变时,MutableState确保使用该状态的可组合(@Composable)函数被重新调用,从而更新UI。这种机制是Compose响应式编程模型的核心,它允许UI自动响应状态的变化。
一些问题
1、为什么compose中需要使用MutableState,而MVVM中的ViewModel需要使用LiveData?
-
MutableState是Compose专门为状态管理设计的一种原语。它是一个容器,持有UI状态的当前值,并且当这个值发生变化时,能够通知Compose系统重新渲染依赖于这个状态的UI部分。这种机制非常适合Compose的声明式和响应式UI更新模式。
-
使用MutableState不仅使状态的更新和UI的重渲染变得高效,而且还紧密集成了Compose的重组机制,确保了轻量级的状态管理并减少了不必要的UI更新。
-
LiveData是一个可观察的数据持有者类,专门设计用于以生命周期感知的方式
命令式编程在ViewModel和UI组件之间共享和管理UI相关数据。当LiveData中的数据变化时,观察这些LiveData的UI组件会收到通知,并据此更新界面。
- LiveData与ViewModel一起使用,支持数据绑定和生命周期感知,减少了内存泄漏的风险和处理生命周期变化的复杂性。它适用于传统的命令式UI更新机制,确保数据变化能够安全、正确地反映到UI上。
2、声明式与命令式编程的差别
命令式编程关注“如何做”,步骤明确。
声明式编程关注“要做什么”,隐藏实现细节。
命令式,如果想根据某个条件改变这个按钮的文本,需要写一个判断逻辑:
// 你需要通过调用例如setXXX等类似的方法来实现View状态的更新,这个就是命令。
if (someCondition) {
myButton.setText("Condition Met");
} else {
myButton.setText("Condition Not Met");
}
如果采用声明式:
@Composable
fun MyButton(showButton: Boolean, condition: Boolean) {
if (showButton) {
Button(onClick = { /* Do something */ }) {
// 你不需要通过命令来实现更新,而只是告诉View,你在这个状态下应该显示成什么样的,这就是声明。
Text(if (condition) "Condition Met" else "Condition Not Met")
}
}
}
通过对比我们发现,我们并没有去手动的set某个view的状态就能够实现了view的更新,这我认为就是命令式与声明式的最大的区别。
参考文献
学后检测
一、单项选择题(每题 2 分)
1. 以下哪一项属于实现工具/类库,而非架构模式?
A. MVC
B. MVP
C. MVVM
D. DataBinding
答案:D
解析: MVC/MVP/MVVM 都是架构模式;DataBinding 是实现 MVVM 思想的库。
2. Android 传统 View 体系中,Activity 同时负责 UI 展示与事件转发的写法更接近于哪种变体?
A. 纯 MVC
B. M-VC
C. Super MVC
D. Passive View
答案:B
解析: XML 功能弱,Activity 同时承担 View 与 Controller,常被称为 M-VC。
3. 在 MVP(Passive View)中,Presenter 更新 View 最常见的技术手段是
A. 反射调用
B. 接口回调
C. 数据绑定
D. LiveData 订阅
答案:B
解析: Passive View 通过接口回调把更新从 Presenter 推给 View。
4. 判断一个页面是否真正采用 MVVM,最关键的标准是
A. 是否使用 androidx.lifecycle.ViewModel
B. 是否启用双向 DataBinding
C. View 是否仅声明 UI 而不持业务状态
D. 是否用 Kotlin 编写
答案:C
解析: MVVM 的核心衡量是职责分层,而非特定库或语言。
5. Jetpack Compose “自带数据驱动”的根本原因是
A. 仍使用 XML
B. SnapshotState + 重组机制
C. 依赖 LiveData
D. 自动生成接口文件
答案:B
解析: Compose 通过 mutableStateOf 等 SnapshotState 容器配合重组,实现声明式、自动的 UI 更新。
二、多项选择题(每题 3 分,少选/错选 0 分)
6. 以下说法能正确体现 MVVM 与 MVP 的差异的是
A. MVVM 通常借助数据绑定自动刷新 UI
B. MVP 中 View 被动订阅状态
C. MVVM 可以使用单向或双向绑定
D. MVP 的 Presenter 同时持有 Model 与 View 接口
答案:A, C, D
解析: 被动订阅是 MVVM 的特点;在 MVP 中 Presenter 主动 push 数据给 View。
7. 关于 Google androidx.lifecycle.ViewModel,以下正确的是
A. 它符合 MVVM 中 ViewModel 职责
B. 使用 MVVM 必须依赖该组件
C. 它支持生命周期感知的状态保存
D. 其他平台可用自定义实现替代
答案:A, C, D
解析: 组件只是实现之一,MVVM 不强依赖;生命周期感知是其优势。
8. 在 MVI 架构的“单向数据流”里,正确的流程包含
A. View 发送 Intent
B. ViewModel 生成新 State
C. View 直接修改 State
D. View 订阅 State 并重新渲染
答案:A, B, D
解析: 数据只能从 View → Intent → ViewModel → 新 State → View,不能反向直接改 State。
9. 下列属于 MVVM 常见“痛点”的有
A. 需要为每个状态写双份 LiveData(Mutable 与只读)
B. View 与 ViewModel 交互分散、难集中管理
C. Presenter 过度膨胀导致接口庞大
D. 强制要求双向数据绑定
答案:A, B, D
解析: C 是 MVP 的典型痛点;MVVM 不一定双向绑定,但官方 DataBinding 倾向双向,易带来模板代码。
10. Compose 中可被框架 自动跟踪并触发重组的状态类型包括
A. mutableStateOf / mutableStateListOf
B. LiveData 配合 observeAsState
C. StateFlow 配合 collectAsState
D. 普通 Kotlin 变量
答案:A, B, C
解析: 普通变量不会触发重组,需放入 SnapshotState、LiveData、Flow 等可观察容器。
三、判断题(每题 2 分,正确写“√”,错误写“×”)
- DataBinding 一旦使用就说明代码肯定符合 MVVM。 ×
解析:仍需看职责划分,View 不能直接操作 Model。 - 在 Android 中,MVC 往往退化为 M-VC,因为 Activity 同时承担 View 与 Controller 的工作。 √
- MVI 的 Model 多指持久化实体,如 User、Order,而非 UI 状态。 ×
解析:MVI 的 Model 就是 ViewState,专指 UI 当前状态快照。 - “声明式 UI”意味着不必再写任何事件回调函数。 ×
解析:事件仍需声明,只是 UI 由状态决定,逻辑仍要处理回调。 - 在 Compose 中,只有使用 LiveData 才能实现响应式更新。 ×
解析:SnapshotState、StateFlow 等都能触发重组。
四、简答题(每题 4 分)
16. 简述 DataBinding 与 MVVM 的关系。
答案要点:
- DataBinding 是实现数据绑定的库;
- 能简化 MVVM 中 View ↔ ViewModel 的同步;
- 是否 MVVM 取决于职责分层,而非是否用了 DataBinding;
- DataBinding 也可服务于 MVP/MVC,只是常与 MVVM 搭配。
17. 为什么 MVP 常被称为 “Passive View”?
答案要点:
- View 不包含业务逻辑,只暴露接口;
- Presenter 主动 push 数据,控制全部流程;
- View 被动接收更新、渲染 UI;
- 强化解耦与可测试性。
18. MVVM 引入 ViewModel 后解决了 MVP 哪些痛点,又带来了哪些新痛点?
答案要点:
- 解决: 去掉接口膨胀;借助绑定自动刷新,减少样板;
- 痛点: 状态膨胀(大量 LiveData);模板代码;双向绑定难调试;复杂页面 ViewModel 过大。
19. 说明 MVI 如何通过单向数据流避免“状态同步失控”。
答案要点:
- Intent → ViewModel → 新 State → 渲染 → 新交互,再循环;
- State 唯一可信源,不可变;
- 无法出现多方并发写状态的竞态;
- 易于回溯、记录、调试。
20. Compose 为什么被称为“声明式 UI”?请给出与命令式写法的对比要点。
答案要点:
- UI = f(State),声明结果而非过程;
- 重组由框架负责;
- 无需显式 setText、setVisibility;
- 命令式需逐步调用 API 更新视图;声明式只需描述当前状态下应展示什么。
21. 在复杂业务页面中,选择 MVP、MVVM、MVI 各有哪些权衡点?
答案要点:
- MVP:接口多,可测试;但膨胀、样板多;
- MVVM:绑定省代码;状态碎片化 & LiveData 双份;
- MVI:统一状态、单向流;State 可能巨大;学习曲线高。
- 选择依业务复杂度、团队习惯、可维护性。
22. 为什么 Compose 推荐使用 mutableStateOf 而不是直接在 ViewModel 暴露可变字段?
答案要点:
- SnapshotState 可以注册 observe,触发重组;
- 普通字段变化无法感知;
- 与重组引擎深度集成,避免额外通知;
- 保持不可变快照,线程安全。
五、编程题(10 分)
要求: 使用 MVI 思想(单向数据流、单一 ViewState、sealed Action)与 Compose,实现一个简单计数器:
- 点击「+」按钮数字加 1,「-」按钮减 1;
- 当计数为偶数时背景显示绿色,为奇数时显示蓝色。
- 请给出 ViewState、Action、ViewModel、Composable 四部分核心代码(可省略样板导包)。
参考答案(Kotlin)
// ViewState:UI 唯一状态
data class CounterState(val count: Int = 0)
// Action:用户意图
sealed interface CounterAction {
object Inc : CounterAction
object Dec : CounterAction
}
// ViewModel:处理 Action -> 新 State
class CounterViewModel : ViewModel() {
private val _uiState = MutableStateFlow(CounterState())
val uiState: StateFlow<CounterState> = _uiState
fun dispatch(action: CounterAction) {
val c = _uiState.value.count
_uiState.value = when (action) {
CounterAction.Inc -> CounterState(c + 1)
CounterAction.Dec -> CounterState(c - 1)
}
}
}
// Composable:声明式渲染
@Composable
fun CounterScreen(vm: CounterViewModel = viewModel()) {
val state by vm.uiState.collectAsState()
val bg = if (state.count % 2 == 0) Color(0xFFAAF0D1) else Color(0xFF87CEFA)
Column(
modifier = Modifier
.fillMaxSize()
.background(bg),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Text("${state.count}", fontSize = 48.sp)
Row {
Button(onClick = { vm.dispatch(CounterAction.Dec) }) { Text("-") }
Spacer(Modifier.width(32.dp))
Button(onClick = { vm.dispatch(CounterAction.Inc) }) { Text("+") }
}
}
}
解析:
- 单一 State:
CounterState; - 单向流:View 按钮 →
dispatch(Action)→ ViewModel 修改_uiState→ View 订阅collectAsState(); - Compose 自动重组,背景色随状态变化。