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());
        }
    }
}

三、每个模块作用详解

模块 职责 为什么这样设计
Model (User) 定义数据结构 与 MVVM 相同,纯 POJO
State (UserState) 包含页面所有 UI 状态的不可变对象 单一状态源,任何时刻 UI 都能从 State 完整还原,避免状态分散导致的不一致
Intent (UserIntent) 定义所有能改变状态的用户操作 所有状态变更必须经过 Intent,形成可追溯的"事件日志",便于调试和埋点
Repository (UserRepository) 数据获取统一入口 与 MVVM 相同,屏蔽数据来源
ViewModel 状态机:接收 Intent → 处理逻辑 → 输出新 State 不暴露多个 LiveData,只暴露一个 State,强制所有状态变更走统一流程
View (Activity) 纯渲染函数 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(可追溯、可预测)
相关推荐
千里马学框架4 天前
一起学 Android 14:ShellTransition 屏幕旋转过程深度剖析
android·智能手机·性能优化·framework·性能·屏幕旋转·rotation
美狐美颜SDK开放平台4 天前
开发直播APP时如何接入视频美颜SDK?开发流程与注意事项
android·人工智能·计算机视觉·音视频·直播美颜sdk
AFinalStone4 天前
Android7 SystemUI源码解析(七)Keyguard锁屏模块深度解析
android·systemui
致远ccc4 天前
Google Play 上架前如何测试 App?多国家 Android 环境测试
android·app测试·googleplay·多国家应用测试
ttyyttemo4 天前
Kotlin 协程中的 Job 结构化并发与取消
android
sun0077004 天前
tbox 4g/5g切换,导致wan ip 改变,导致车机旧网络不可用。需要重启车机才行
android
其实防守也摸鱼4 天前
内网穿透与反向代理:原理、工具与实战指南
android·大数据·运维·安全·网络安全·自动化·渗透
AFinalStone4 天前
Android7 SystemUI 源码解析(四)NavigationBar 导航栏与 SystemBars
android·systemui
JMchen4 天前
属性动画原理与高级动画实现
android·kotlin·canvas
AFinalStone4 天前
Android7 SystemUI 源码解析(二)启动流程深度解析
android·systemui