软件架构发展史之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在内的数据、状态修改较分散的问题,它更像是一种全新理念的尝试。

相关推荐
lingggggaaaa8 小时前
PHP模型开发篇&MVC层&RCE执行&文件对比法&1day分析&0day验证
开发语言·学习·安全·web安全·php·mvc
2601_954043721 天前
深入解析 JamTools:免费开源聚合工具的技术架构与跨平台实现
软件架构·工具开发·聚合工具
EFCY1MJ901 天前
ASP.NET MVC 1.0 (五) ViewEngine 深入解析与应用实例
后端·asp.net·mvc
小江的记录本1 天前
【RabbitMQ】RabbitMQ核心知识体系全解(5大核心模块:Exchange类型、消息确认机制、死信队列、延迟队列、镜像队列)
java·前端·分布式·后端·spring·rabbitmq·mvc
小信丶3 天前
Spring MVC @SessionAttributes 注解详解:用法、场景与实战示例
java·spring boot·后端·spring·mvc
seven97_top3 天前
MVC快速入门
mvc
极创信息5 天前
信创软件安全加固指南,信创软件的纵深防御体系
java·大数据·数据库·金融·php·mvc·软件工程
hnlgzb5 天前
MVC和MVVM设计模式中对应的是什么组件?有什么对应关系?
设计模式·mvc
zs宝来了6 天前
Spring MVC 请求处理全流程:从 DispatcherServlet 到视图渲染
spring·mvc·源码解析·dispatcherservlet