Android随笔-MVI

一、MVI 是什么

MVI(Model-View-Intent)是一种基于单向数据流的架构模式,核心思想是:

  • Intent:用户意图/事件(如点击、输入、刷新),是改变状态的唯一来源
  • ViewModel:接收 Intent,处理业务逻辑,生成新的 State
  • State:不可变的单一状态对象,包含页面所有 UI 信息
  • View:观察 State,纯函数式渲染 UI,不持有业务逻辑

核心公式:View = f(State) ------ UI 只是状态的纯函数映射。

二、完整 Demo:用户状态管理页面

实现一个用户信息页面,演示 MVI 的完整数据流。

1. 依赖配置(build.gradle)

groovy 复制代码
apply plugin: 'com.android.application'

android {
    compileSdkVersion 33
    defaultConfig {
        applicationId "com.example.mvidemo"
        minSdkVersion 21
        targetSdkVersion 33
    }
    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }
}

dependencies {
    implementation 'androidx.appcompat:appcompat:1.6.1'
    implementation 'com.google.android.material:material:1.8.0'
    // ViewModel + LiveData
    implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'
    annotationProcessor 'androidx.lifecycle:lifecycle-compiler:2.2.0'
}

2. Model 层

User.java

java 复制代码
package com.example.mvidemo.model;

public class User {
    private String name;
    private int age;
    private String bio;

    public User(String name, int age, String bio) {
        this.name = name;
        this.age = age;
        this.bio = bio;
    }

    public String getName() { return name; }
    public void setName(String name) { this.name = name; }
    public int getAge() { return age; }
    public void setAge(int age) { this.age = age; }
    public String getBio() { return bio; }
    public void setBio(String bio) { this.bio = bio; }
}

3. State 层(MVI 核心)

UserState.java ------ 单一不可变状态

java 复制代码
package com.example.mvidemo.state;

import com.example.mvidemo.model.User;

/**
 * State:页面的唯一状态源
 * 特点:
 * 1. 不可变(Immutable):状态变更时必须创建新对象
 * 2. 包含页面所有 UI 信息:数据、加载态、错误信息
 * 3. View 只需观察这一个对象即可渲染完整界面
 */
public class UserState {
    
    // 用户数据
    private final User user;
    // 是否加载中
    private final boolean loading;
    // 错误信息(null 表示无错误)
    private final String error;
    // 操作成功提示(如保存成功)
    private final String successMessage;
    
    // 私有构造器,强制通过 Builder 创建
    private UserState(Builder builder) {
        this.user = builder.user;
        this.loading = builder.loading;
        this.error = builder.error;
        this.successMessage = builder.successMessage;
    }
    
    // ========== Getter(只有 Getter,没有 Setter,保证不可变)==========
    
    public User getUser() { return user; }
    public boolean isLoading() { return loading; }
    public String getError() { return error; }
    public String getSuccessMessage() { return successMessage; }
    
    // ========== Builder 模式:基于当前状态创建新状态 ==========
    
    public Builder copy() {
        return new Builder(this);
    }
    
    public static Builder builder() {
        return new Builder();
    }
    
    public static class Builder {
        private User user;
        private boolean loading;
        private String error;
        private String successMessage;
        
        public Builder() {}
        
        public Builder(UserState state) {
            this.user = state.user;
            this.loading = state.loading;
            this.error = state.error;
            this.successMessage = state.successMessage;
        }
        
        public Builder user(User user) {
            this.user = user;
            return this;
        }
        
        public Builder loading(boolean loading) {
            this.loading = loading;
            return this;
        }
        
        public Builder error(String error) {
            this.error = error;
            return this;
        }
        
        public Builder successMessage(String msg) {
            this.successMessage = msg;
            return this;
        }
        
        public UserState build() {
            return new UserState(this);
        }
    }
    
    // 空状态(初始状态)
    public static UserState idle() {
        return builder().build();
    }
}

4. Intent 层(MVI 核心)

UserIntent.java ------ 用户意图密封类

java 复制代码
package com.example.mvidemo.intent;

import com.example.mvidemo.model.User;

/**
 * Intent:用户意图/事件
 * 在 Java 中通过抽象类 + 静态内部类模拟 Kotlin 的密封类(Sealed Class)
 * 
 * 所有能改变状态的操作,必须定义为一个 Intent:
 * - 用户点击按钮 → Intent
 * - 页面初始化 → Intent  
 * - 输入框变化 → Intent
 * 
 * ViewModel 只接受 Intent,不接受直接的方法调用
 */
public abstract class UserIntent {
    
    /** 加载用户 */
    public static class LoadUser extends UserIntent {
        private final String userId;
        
        public LoadUser(String userId) {
            this.userId = userId;
        }
        
        public String getUserId() {
            return userId;
        }
    }
    
    /** 更新用户名 */
    public static class UpdateName extends UserIntent {
        private final String newName;
        
        public UpdateName(String newName) {
            this.newName = newName;
        }
        
        public String getNewName() {
            return newName;
        }
    }
    
    /** 保存用户 */
    public static class SaveUser extends UserIntent {}
    
    /** 刷新页面 */
    public static class Refresh extends UserIntent {}
    
    /** 清除错误(用户已消费错误提示) */
    public static class ClearError extends UserIntent {}
}

5. Repository 层

UserRepository.java

java 复制代码
package com.example.mvidemo.repository;

import android.os.Handler;
import android.os.Looper;
import com.example.mvidemo.model.User;

/**
 * Repository:数据仓库
 * 职责:屏蔽数据来源,为 ViewModel 提供统一数据接口
 */
public class UserRepository {
    
    public void getUserFromServer(String userId, Callback callback) {
        new Thread(() -> {
            try {
                Thread.sleep(1500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            
            // 模拟 20% 概率失败
            if (Math.random() > 0.8) {
                new Handler(Looper.getMainLooper()).post(() -> {
                    callback.onError("网络连接失败,请重试");
                });
                return;
            }
            
            User user = new User("张三", 25, "Android 开发工程师,热爱技术");
            new Handler(Looper.getMainLooper()).post(() -> {
                callback.onSuccess(user);
            });
        }).start();
    }
    
    public void saveUserToServer(User user, Callback callback) {
        new Thread(() -> {
            try {
                Thread.sleep(800);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            new Handler(Looper.getMainLooper()).post(() -> {
                callback.onSuccess(null);
            });
        }).start();
    }
    
    public interface Callback {
        void onSuccess(User user);
        void onError(String error);
    }
}

6. ViewModel 层(MVI 核心)

UserViewModel.java

java 复制代码
package com.example.mvidemo.viewmodel;

import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.ViewModel;
import com.example.mvidemo.intent.UserIntent;
import com.example.mvidemo.model.User;
import com.example.mvidemo.repository.UserRepository;
import com.example.mvidemo.state.UserState;

/**
 * ViewModel:状态处理器
 * 
 * 核心职责:
 * 1. 持有唯一的 State(通过 LiveData 暴露)
 * 2. 接收 Intent,处理业务逻辑,生成新的不可变 State
 * 3. 不持有 View 引用,不直接操作 UI
 * 
 * MVI 中的 ViewModel 是一个"状态机":
 *    Intent → 处理逻辑 → 新 State → View 自动刷新
 */
public class UserViewModel extends ViewModel {
    
    // 单一状态源:页面所有 UI 信息都在这个对象里
    private final MutableLiveData<UserState> stateLiveData = new MutableLiveData<>(UserState.idle());
    
    private final UserRepository repository = new UserRepository();
    
    /** 对外暴露不可变的 State */
    public LiveData<UserState> getState() {
        return stateLiveData;
    }
    
    /**
     * 唯一入口:处理用户意图
     * 所有能改变状态的操作,都必须通过这个方法
     */
    public void processIntent(UserIntent intent) {
        if (intent instanceof UserIntent.LoadUser) {
            handleLoadUser((UserIntent.LoadUser) intent);
        } else if (intent instanceof UserIntent.UpdateName) {
            handleUpdateName((UserIntent.UpdateName) intent);
        } else if (intent instanceof UserIntent.SaveUser) {
            handleSaveUser();
        } else if (intent instanceof UserIntent.Refresh) {
            handleRefresh();
        } else if (intent instanceof UserIntent.ClearError) {
            handleClearError();
        }
    }
    
    // ========== 私有方法:处理具体 Intent,生成新 State ==========
    
    private void handleLoadUser(UserIntent.LoadUser intent) {
        // 1. 基于当前状态,创建"加载中"的新状态
        UserState current = stateLiveData.getValue();
        UserState loadingState = current.copy()
                .loading(true)
                .error(null)
                .build();
        stateLiveData.setValue(loadingState);
        
        // 2. 请求数据
        repository.getUserFromServer(intent.getUserId(), new UserRepository.Callback() {
            @Override
            public void onSuccess(User user) {
                // 3. 成功:创建"加载完成+有数据"的新状态
                UserState successState = stateLiveData.getValue().copy()
                        .user(user)
                        .loading(false)
                        .build();
                stateLiveData.setValue(successState);
            }
            
            @Override
            public void onError(String error) {
                // 4. 失败:创建"加载完成+有错误"的新状态
                UserState errorState = stateLiveData.getValue().copy()
                        .loading(false)
                        .error(error)
                        .build();
                stateLiveData.setValue(errorState);
            }
        });
    }
    
    private void handleUpdateName(UserIntent.UpdateName intent) {
        UserState current = stateLiveData.getValue();
        User user = current.getUser();
        if (user != null) {
            user.setName(intent.getNewName());
            // 创建新状态对象(不可变原则)
            UserState newState = current.copy()
                    .user(user)
                    .build();
            stateLiveData.setValue(newState);
        }
    }
    
    private void handleSaveUser() {
        UserState current = stateLiveData.getValue();
        User user = current.getUser();
        if (user == null) return;
        
        stateLiveData.setValue(current.copy().loading(true).build());
        
        repository.saveUserToServer(user, new UserRepository.Callback() {
            @Override
            public void onSuccess(User user) {
                UserState newState = stateLiveData.getValue().copy()
                        .loading(false)
                        .successMessage("保存成功!")
                        .build();
                stateLiveData.setValue(newState);
            }
            
            @Override
            public void onError(String error) {
                UserState newState = stateLiveData.getValue().copy()
                        .loading(false)
                        .error(error)
                        .build();
                stateLiveData.setValue(newState);
            }
        });
    }
    
    private void handleRefresh() {
        UserState current = stateLiveData.getValue();
        User user = current.getUser();
        if (user != null) {
            processIntent(new UserIntent.LoadUser("refresh"));
        }
    }
    
    private void handleClearError() {
        UserState current = stateLiveData.getValue();
        stateLiveData.setValue(current.copy().error(null).successMessage(null).build());
    }
}

7. View 层

activity_main.xml

xml 复制代码
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:padding="20dp">

    <ProgressBar
        android:id="@+id/progressBar"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center"
        android:visibility="gone" />

    <TextView
        android:id="@+id/tvName"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textSize="24sp"
        android:textStyle="bold" />

    <TextView
        android:id="@+id/tvAge"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textSize="18sp"
        android:layout_marginTop="8dp" />

    <TextView
        android:id="@+id/tvBio"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textSize="16sp"
        android:layout_marginTop="8dp" />

    <Button
        android:id="@+id/btnLoad"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="16dp"
        android:text="加载用户" />

    <Button
        android:id="@+id/btnUpdate"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="8dp"
        android:text="修改名字为 李四" />

    <Button
        android:id="@+id/btnSave"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="8dp"
        android:text="保存到服务器" />
</LinearLayout>

MainActivity.java

java 复制代码
package com.example.mvidemo;

import android.os.Bundle;
import android.view.View;
import android.widget.ProgressBar;
import android.widget.TextView;
import android.widget.Toast;
import androidx.appcompat.app.AppCompatActivity;
import androidx.lifecycle.ViewModelProvider;
import com.example.mvidemo.intent.UserIntent;
import com.example.mvidemo.state.UserState;
import com.example.mvidemo.viewmodel.UserViewModel;

/**
 * View 层:Activity
 * 
 * 核心职责:
 * 1. 初始化 ViewModel
 * 2. 观察单一 State,根据 State 渲染完整 UI
 * 3. 将用户操作转换为 Intent,发送给 ViewModel
 * 
 * 设计原则:
 * - 不持有业务逻辑
 * - 不直接修改数据
 * - UI 只是 State 的纯函数映射:render(State) → UI
 */
public class MainActivity extends AppCompatActivity {
    
    private UserViewModel viewModel;
    
    private ProgressBar progressBar;
    private TextView tvName, tvAge, tvBio;
    
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        
        // 初始化 View
        progressBar = findViewById(R.id.progressBar);
        tvName = findViewById(R.id.tvName);
        tvAge = findViewById(R.id.tvAge);
        tvBio = findViewById(R.id.tvBio);
        
        // 获取 ViewModel
        viewModel = new ViewModelProvider(this).get(UserViewModel.class);
        
        // ========== 核心:观察单一 State,驱动 UI 渲染 ==========
        viewModel.getState().observe(this, this::render);
        
        // ========== 用户操作 → 转换为 Intent → 发送给 ViewModel ==========
        
        findViewById(R.id.btnLoad).setOnClickListener(v -> {
            viewModel.processIntent(new UserIntent.LoadUser("user_001"));
        });
        
        findViewById(R.id.btnUpdate).setOnClickListener(v -> {
            viewModel.processIntent(new UserIntent.UpdateName("李四"));
        });
        
        findViewById(R.id.btnSave).setOnClickListener(v -> {
            viewModel.processIntent(new UserIntent.SaveUser());
        });
    }
    
    /**
     * 渲染函数:根据 State 刷新完整 UI
     * 这是 MVI 的核心:View = f(State)
     */
    private void render(UserState state) {
        // 1. 渲染加载状态
        progressBar.setVisibility(state.isLoading() ? View.VISIBLE : View.GONE);
        
        // 2. 渲染用户数据
        if (state.getUser() != null) {
            tvName.setText(state.getUser().getName());
            tvAge.setText(state.getUser().getAge() + " 岁");
            tvBio.setText(state.getUser().getBio());
        }
        
        // 3. 渲染错误信息
        if (state.getError() != null) {
            Toast.makeText(this, "错误:" + state.getError(), Toast.LENGTH_SHORT).show();
            // 消费后清除,避免重复弹出
            viewModel.processIntent(new UserIntent.ClearError());
        }
        
        // 4. 渲染成功提示
        if (state.getSuccessMessage() != null) {
            Toast.makeText(this, state.getSuccessMessage(), Toast.LENGTH_SHORT).show();
            viewModel.processIntent(new UserIntent.ClearError());
        }
    }
}

三、每个模块作用详解

模块 职责 为什么这样设计
ModelUser 定义数据结构 与 MVVM 相同,纯 POJO
StateUserState 包含页面所有 UI 状态的不可变对象 单一状态源,任何时刻 UI 都能从 State 完整还原,避免状态分散导致的不一致
IntentUserIntent 定义所有能改变状态的用户操作 所有状态变更必须经过 Intent,形成可追溯的"事件日志",便于调试和埋点
RepositoryUserRepository 数据获取统一入口 与 MVVM 相同,屏蔽数据来源
ViewModel 状态机:接收 Intent → 处理逻辑 → 输出新 State 不暴露多个 LiveData,只暴露一个 State,强制所有状态变更走统一流程
ViewActivity 纯渲染函数 render(State),发送 Intent 不包含任何 if/else 业务判断,只负责把 State 映射到 UI 控件

四、MVI 的数据流向图

复制代码
用户点击按钮
    │
    ▼
┌─────────────┐
│   Intent    │  ← 用户意图(LoadUser/UpdateName/SaveUser)
└──────┬──────┘
       │
       ▼
┌─────────────┐
│  ViewModel   │
│ processIntent│
└──────┬──────┘
       │
       ▼
┌─────────────┐
│  业务逻辑处理  │  ← 调用 Repository、计算、转换
└──────┬──────┘
       │
       ▼
┌─────────────┐
│  New State   │  ← 全新的不可变状态对象
│(copy + 修改)│
└──────┬──────┘
       │
       ▼
┌─────────────┐
│   render()   │  ← Activity 中的纯函数
└──────┬──────┘
       │
       ▼
    UI 刷新

关键特征

  • 单向:数据永远从 Intent → ViewModel → State → View,不会反向流动
  • 不可变:State 一旦创建不可修改,变更必须创建新对象
  • 纯函数:render(State) 只依赖输入的状态,不依赖外部变量

五、MVI 的优势与缺点

✅ 优势

1. 单一状态源(Single Source of Truth)

页面所有状态收敛到一个 State 对象,不会出现"数据不一致"的 Bug

2. 状态可追溯、可预测

每个 UI 状态都是由 Intent 经过纯函数计算得出,给定相同的 Intent 序列,一定得到相同的 State 序列

3. 单向数据流,易于调试
  • 数据流向单一,不会形成 MVVM 中多个 LiveData 交叉观察的"回调地狱"
  • 可以记录所有 Intent 序列,实现"时间旅行调试"(Time Travel Debugging)
4. 渲染逻辑集中

所有 UI 更新集中在 render() 一个函数中,代码结构清晰

5. 天然支持响应式编程

与 RxJava、Kotlin Flow、Compose 等响应式框架思想高度契合

❌ 缺点

1. 样板代码多
  • 每个页面都需要定义 State、Intent、render(),简单页面显得过重
  • Java 中缺乏密封类和 Data Class,实现不可变 State 需要写大量 Builder 代码
2. State 膨胀问题
  • 随着页面复杂度增加,State 类会变得非常大,包含大量字段
  • 一次小的状态变更也需要创建完整的 State 新对象,存在性能开销
3. 一次性事件处理麻烦

Toast、Snackbar、页面跳转等"一次性事件"不适合放在 State 中(因为配置变更后会重发)

需要引入 SingleLiveEvent 或 Channel 等额外机制

4. 学习曲线陡峭

需要理解函数式编程、不可变数据、单向数据流等概念

5. 不适合超复杂页面

如果页面有多个独立模块(如头部、列表、底部各不相关),单一 State 会导致不必要的全量刷新

六、MVVM vs MVI 对比

维度 MVVM MVI
数据流 双向(View ↔ ViewModel) 单向(Intent → State → View)
状态管理 多个 LiveData,分散管理 单一 State 对象,集中管理
可变性 LiveData 的值可变 State 不可变,必须创建新对象
调试难度 多个数据流交叉,较难追踪 单一数据流,Intent 序列可追溯
代码量 较少 较多(State + Intent + render)
适用场景 大多数业务页面 状态复杂、需要严格状态管理的页面
与 Compose 配合 可用 更契合(State 驱动重组)

七、常见问题

1. MVI 和 MVVM 的本质区别是什么?

MVVM 是"多数据流":多个 LiveData 各自独立,View 观察多个数据源。MVI 是"单一数据流":所有状态收敛为一个不可变的 State,通过 Intent 驱动变更,UI 只是 State 的纯函数映射。

2. 什么是单向数据流?为什么 MVI 要强调这个?

数据永远从 Intent → ViewModel → State → View 单向流动。这样可以保证状态变更可追溯、可预测,避免 MVVM 中多个 LiveData 互相观察导致的"回调地狱"和状态不一致。

3. MVI 中的 State 为什么要设计为不可变?

不可变对象保证线程安全,避免并发修改问题;同时保证状态变更的"原子性"------任何时候 State 都是完整的、一致的,不会出现"改了一半"的中间状态被 UI 渲染。

4. MVI 如何处理一次性事件(如 Toast、页面跳转)?

一次性事件不适合放在 State 中,因为 State 是持久化的,配置变更(如旋转屏幕)后会重发事件。解决方案:

  • 使用 SingleLiveEvent 单独处理事件流
  • 使用 Kotlin 的 Channel / SharedFlow
  • 在 State 中增加 consumable 标记位,消费后自动清除

5. State 膨胀了怎么办?

对于超复杂页面,可以将 State 拆分为多个子 State(如 HeaderState、ListState),但每个模块内部仍保持单一 State 原则。或者退化为 MVVM,对独立模块使用独立的 ViewModel。

6. MVI 中 ViewModel 的 processIntent() 方法有什么作用?

它是 ViewModel 的唯一入口,所有能改变状态的操作都必须封装为 Intent 通过此方法传入。这样 ViewModel 就是一个纯"状态机":接收输入(Intent)→ 处理逻辑 → 输出状态(State)。

7. Java 中如何实现 Kotlin 的密封类(Sealed Class)?

将 Intent 定义为 abstract class,所有具体意图定义为 public static class 内部类。外部只能通过这些内部类实例化,达到类似密封类的效果。

8. MVI 适合所有项目吗?

不适合。对于简单页面(如只有一两个按钮和文本),MVI 的样板代码(State + Intent + Builder)会显得过重。推荐在状态复杂、多人协作、需要严格状态管理的页面使用 MVI。

9. MVI 和 Redux 有什么关系?

MVI 的思想来源于 Redux(Flux 架构)。Redux 的核心也是:Action → Reducer → Store → View。MVI 的 Intent 对应 Action,ViewModel 对应 Store + Reducer,State 对应 Store 中的 State。

10. 如何在 MVI 中处理副作用(Side Effect)?

副作用指异步操作(网络请求、数据库读写)。在 MVI 中,副作用通常在 ViewModel 的 processIntent() 中触发,通过回调或协程获取结果后,再生成新的 State。更严格的实现会引入 Effect 概念,将副作用与状态变更分离。

八、总结

  • MVVM 是"多数据流观察":View 观察多个 LiveData,各自独立更新。
  • MVI 是"单一状态机":所有状态收敛为一个不可变的 State,通过 Intent 驱动变更,UI 只是 render(State) 的纯函数映射。
  • 普通业务页面 → MVVM(代码量少,足够用)
  • 状态复杂、需要严格状态管理、配合 Compose → MVI(可追溯、可预测)
相关推荐
leobertlan2 小时前
【好玩系列】训练一个神经网络指导小孩玩游戏
android·机器学习·程序员
Android-Flutter3 小时前
android LeakCanary 工作原理 详解
android·kotlin
newcih3 小时前
从原理到实战:LSPosed 模块开发完全指南(2026 修订版)
android·xposed·lsposed
AFinalStone4 小时前
Android 7系统无障碍服务(一)全景图与架构概览
android·无障碍服务
Android-Flutter4 小时前
android 自定义view 详解
android·kotlin
WAsbry5 小时前
协程任务的失败控制:取消、异常传播与Supervisor
android
Android打工仔5 小时前
从 finally 理解程序的控制流:它为什么不是 catch 后面的代码?
android·kotlin
随遇丿而安5 小时前
第15周:Service 全功能 + 后台优化
android
YXL1111YXL5 小时前
续体和状态机 —— suspend 函数的 CPS 变换
android·kotlin