软件架构发展史之MVC/MVP/MVVM/MVI

在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的核心特点

  1. 业务逻辑仍在 Model
  2. 状态管理在 ViewModel
  3. UI 更新自动响应状态变化
  4. 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在内的数据、状态修改较分散的问题,它更像是一种全新理念的尝试。

相关推荐
budingxiaomoli20 小时前
Spring Web MVC 知识总结
spring·mvc
虾米Life2 天前
MVC与MVVM 架构
架构·mvc·mvvm
笛卡尔的心跳4 天前
Spring MVC 注解
java·spring·mvc
小松加哲4 天前
Spring MVC 核心原理全解析
java·spring·mvc
那个失眠的夜5 天前
RESTful 语法规范 核心注解详解
java·spring·mvc·mybatis
羌俊恩5 天前
Centos环境django项目部署过程
django·flask·centos·mvc·mtv·web项目框架
Foreer黑爷7 天前
Spring MVC原理与源码:从请求到响应的全流程解析
java·spring·mvc
曹牧8 天前
Spring MVC中使用HttpServletRequest和HttpServletResponse
java·spring·mvc
曹牧8 天前
Spring MVC配置文件
java·spring·mvc
CPUOS20108 天前
嵌入式C语言高级编程之MVC设计模式
c语言·设计模式·mvc