Android中页面架构MVC、MVP、MVVM、MVI、Compose的应用与区别

883 阅读31分钟

对于一个应用,内部可以有多个架构模式并存,而不是一定要所有的页面都用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的整体架构如下:

image.png

一个简单,但是却能清晰的表明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 更新数据。
      这样的职责划分简单清晰,没有额外的业务逻辑或数据处理流程。
  • 数据流与调用链路简单

    • 用户操作(View)→ clicked() → (通知) → Controller
    • Controller → onViewClicked() → (更新) → Model
    • Model → processData() → (更新数据并通知) → View
      数据流在这三者之间形成一个最基本的环路,整个调用过程易于理解和追踪。

不过一般的,我们在Android中写代码的时候,一般不会将View和Model直接写在一起。所以我们平常所说的把所有业务逻辑和界面更新操作都写进 Activity 的写法并不是标准的MVC,这种模式应该叫做M-VC。

在安卓页面中,MVC的分工:

M:Model 单独文件/package

V: View XML

C: Controller Activity/Fragment

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

image.png

标准的MVC结构具备了:

优点:结构清晰、简单,分工明确,

缺点:模块之间存在耦合;特别的,在安卓中Activity同时负责ViewController层的工作,违背了单一职责原则。

应用场景:设置/历史订单/交易记录/收藏,因为这些场景的逻辑简单。

MVP

由于MVC架构在Android平台上的一些缺陷,因此MVP架构应运而生,MVP通过代码分层将Activity中的各项职能隔离开来,以接口回调的形式进行数据通信。 MVP的整体架构如下:

image.png

image.png M: Model

V: View: xml + activity + view的interface

P: Presenter 逻辑 view和model的交互

  1. View层:对应于ActivityXML,只负责显示UI,只与Presenter层交互,与Model层没有耦合;
  2. Presenter层: 主要负责处理业务逻辑,通过接口回调View层;
  3. 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 的引用,用来驱动业务;
    • 持有对 ViewMyView 接口)的引用,用来回调更新 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);
    }
}

image.png

image.png

image.png

持有的关系: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架构图如下所示:

image.png

  1. View观察ViewModel的数据变化并自我更新,这其实是单一数据源而不是双向数据绑定,所以其实MVVM的这一大特性很多时候并没有用到;
  2. View通过调用ViewModel提供的方法来与ViewMdoel交互。

image.png

进一步

即便不使用Databinding,我们也能实现数据与视图的双向绑定,因为MVVM的核心就是数据驱动UI,UI变化更新数据, 什么是数据绑定:

  1. 外部数据(数据库数据、⽹络数据)、内存数据(Java 代码中的变量)、表现数据(界⾯中展示的数据)中的外部表现数据和内存数据互相⾃动更新。
  2. 另外,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。

  1. Model 层

    • 例如 StringAttr 类,仅用于保存和管理数据。它通过 onChangeListener 通知数据变化,这部分只关注数据状态,不涉及 UI 展示。
  2. View 层

    • 例如 MvvmActivity 以及布局中的 EditText,只负责展示界面和接收用户输入,不包含业务逻辑。
  3. ViewModel 层

    • ViewModel 类作为中介,负责将 Model 和 View 关联起来。它使用 ViewBinder 实现双向绑定,将 UI 控件(EditText)的变化同步到数据模型(StringAttr),反过来数据变化也自动更新到 UI。这种数据绑定使得业务逻辑与界面展示彻底解耦,符合 MVVM 的核心思想。
  4. 双向数据绑定

    • 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

优点:角色清晰

缺点:ViewViewModel通过ViewModel暴露的方法交互,比较零乱难以维护

应用场景:首页/复杂需求多变的页面

测试代码:github.com/xingchaozha…

小结

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

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

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

  • MVVMMVP的主要区别在于,你不用去主动去刷新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架构主要有以下不足:

  1. 为保证对外暴露的LiveData是不可变的,需要添加不少模板代码并且容易遗忘
  2. View层与ViewModel层的交互比较分散零乱,不成体系

MVI 与 MVVM 很相似,其借鉴了前端框架的思想,更加强调数据的单向流动和唯一数据源,它通过引入不可变状态和统一的Action来确保数据只单向流动。因为我们很多时候并不需要实现UI更新后同步更新数据的逻辑。

image.png

其主要分为以下几部分

  1. Model: 与MVVM中的Model不同的是,MVIModel主要指UI状态(State)。例如页面加载状态、控件位置等都是一种UI状态,接收 Intent,处理业务逻辑,并生成一个全新的、不可变的状态。
  2. View: 与其他MVX中的View一致,可能是一个Activity或者任意UI承载单元。MVI中的View通过订阅Model的变化,并根据新的状态重新渲染 UI。
  3. Intent: 此Intent不是ActivityIntent,用户的任何操作都被包装成Intent后发送给Model层进行数据请求,捕捉用户的各种操作(如点击、滑动等),将其转换为明确的用户意图。
  4. 循环数据流:用户在 View 上的交互再次产生新的 Intent,形成单向循环数据流。

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

  1. 用户操作以Intent的形式通知Model
  2. Model基于Intent更新State
  3. View接收到State变化刷新UI。

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

image.png

下面提供一个简单的示例,展示如何使用 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通过ActionViewModel交互,通过 Action 通信,有于 View 与 ViewModel 之间的进一步解耦,同时所有调用以 Action 的形式汇总到一处,也有利于对行为的集中分析和监控。

在 MVI 里,M 确实是指 “Model”,但它跟我们在领域层(Domain)里说的 “Model” 并不完全一样——它就是专门用来描述UI 当前状态(State)的那份 “模型”。

  • 我们把它叫做 ViewStateUiState,只是为了强调它是 UI 的「状态快照」,而不是业务层的实体(比如 UserOrderProduct)。

  • 本质上,这个 ViewState 就是 MVI 架构中的 Model:

    1. 它是 单一可信源(Single Source of Truth),
    2. 它保存并描述了所有需要在界面上渲染的数据,
    3. 每次发生变化时都会生成一个新的不可变对象。

之所以不直接叫 Model 而用 ViewState/UiState,主要是为了和其他层面的“Model”区分开来,让人一看就知道这是 MVI 里的那份“UI 模型/状态”,而不是后端数据模型。

换句话说:

MVI 的 “M” = “ViewState”  
I = “Intent”(用户意图/事件)  
V = “View”(渲染层)  

所以,把它命名为 MyViewState 并不会违背 MVI,只是让代码语义更清晰、更自解释。

小结MVC,MVP,MVVMMVI架构

目前MVVM是官方推荐的架构,但仍然有以下几个痛点:

  1. MVVMMVP的主要区别在于双向数据绑定,但由于很多人不喜欢使用DataBindg,其实并没有使用MVVM双向绑定的特性,而是单一数据源
  2. 当页面复杂时,需要定义很多State,并且需要定义可变与不可变两种,状态会以双倍的速度膨胀,模板代码较多且容易遗忘
  3. ViewViewModel通过ViewModel暴露的方法交互,比较零乱难以维护

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

  1. 强调数据单向流动,很容易对状态变化进行跟踪和回溯
  2. 使用ViewStateState集中管理,只需要订阅一个 ViewState 便可获取页面的所有状态,相对 MVVM 减少了不少模板代码
  3. ViewModel通过ViewStateAction通信,通过浏览ViewStateAciton 定义就可以理清 ViewModel 的职责,可以直接拿来作为接口文档使用。

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

  1. 所有的操作最终都会转换成State,所以当复杂页面的State容易膨胀
  2. 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 能自动跟踪的状态类型

  1. SnapshotState(mutableStateOf / mutableStateListOf 等)

    var count by mutableStateOf(0)
    val list = mutableStateListOf<String>()
    
  2. LiveData

    // ViewModel
    private val _foo = MutableLiveData<String>()
    val foo: LiveData<String> = _foo
    // Compose 中
    val fooValue by viewModel.foo.observeAsState("")
    
  3. StateFlow / Flow

    // ViewModel
    private val _bar = MutableStateFlow(0)
    val bar: StateFlow<Int> = _bar
    // Compose 中
    val barValue by viewModel.bar.collectAsState()
    
  4. 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自动响应状态的变化。

测试代码:github.com/xingchaozha…

一些问题

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的更新,这我认为就是命令式与声明式的最大的区别。

参考文献

juejin.cn/post/702262…

学后检测

一、单项选择题(每题 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 分,正确写“√”,错误写“×”)

  1. DataBinding 一旦使用就说明代码肯定符合 MVVM。 ×
    解析:仍需看职责划分,View 不能直接操作 Model。
  2. 在 Android 中,MVC 往往退化为 M-VC,因为 Activity 同时承担 View 与 Controller 的工作。
  3. MVI 的 Model 多指持久化实体,如 User、Order,而非 UI 状态。 ×
    解析:MVI 的 Model 就是 ViewState,专指 UI 当前状态快照。
  4. “声明式 UI”意味着不必再写任何事件回调函数。 ×
    解析:事件仍需声明,只是 UI 由状态决定,逻辑仍要处理回调。
  5. 在 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("+") }
        }
    }
}

解析:

  • 单一 StateCounterState
  • 单向流:View 按钮 → dispatch(Action) → ViewModel 修改 _uiState → View 订阅 collectAsState()
  • Compose 自动重组,背景色随状态变化。