架构总览
本项目采用 MVI(Model-View-Intent)架构,基于 Jetpack Compose 构建声明式 UI 层。MVI 架构借鉴了前端框架(如 React/Redux)的思想,强调单向数据流和唯一数据源,与 Compose 的声明式 UI 思想高度契合 README.md:17-18。
MVI 架构概述
MVI 架构将应用分为三层核心组件:Model、View 和 Intent,通过严格的单向数据流约束各层职责边界 README.md:17-23。
核心组件职责
| 组件 | 职责 | 关键特征 |
|---|---|---|
| Model | 管理 UI 状态(State),如页面加载状态、控件位置等 | 唯一数据源,集中管理所有 UI 状态 |
| View | UI 承载单元(Activity/Composable),订阅 Model 变化实现界面刷新 | 被动响应状态变化,不直接调用 ViewModel 方法 |
| Intent | 封装用户操作,发送给 Model 层进行数据请求 | 不是 Activity 的 Intent,而是用户意图的抽象 |
与 MVVM 的关键差异
MVI 在 MVVM 基础上进行了架构约束强化 README.md:60-62:
- 交互方式约束:MVVM 允许 View 层随意调用 ViewModel 方法,而 MVI 屏蔽 ViewModel 实现,只能通过发送 Intent 驱动事件
- 状态集中管理:MVVM 在 ViewModel 中分散定义多个 State,MVI 使用 ViewState 集中管理,只需订阅一个 ViewState 即可获取页面所有状态
正在加载图表渲染器...
架构图要点说明:
- 单向数据流:用户操作 → Intent → StateProcessor → ViewState → View 更新,形成闭环
- 唯一数据源:ViewState 作为唯一状态源,所有 UI 组件订阅同一状态实例
- 事件分离:ViewEvent 处理 Toast、页面关闭等一次性事件,与持续性状态 ViewState 分离
- ViewModel 屏蔽:View 层不直接调用 ViewModel 方法,通过 Intent 解耦
核心模块详解
状态管理模块
职责边界:集中管理页面所有 UI 状态,作为唯一数据源供 View 层订阅。不包含业务逻辑,仅负责状态定义与存储。
入口与关键 API:
- 数据类:
LoginViewState(登录页面状态定义)README.md:30-34 - 状态字段:
account(账号)、password(密码)、isLogged(登录状态)
关键数据结构:
kotlin1data class LoginViewState( 2 val account: String = "", // 用户输入的账号 3 val password: String = "", // 用户输入的密码 4 val isLogged: Boolean = false // 登录成功标志 5)
交互方式:View 层通过 Compose 的 collectAsState() 订阅 ViewState 变化,状态更新自动触发 UI 重组。
意图封装模块
职责边界:将所有用户操作封装为类型安全的 Intent 对象,定义 View 层可触发的操作范围。不处理操作执行逻辑。
入口与关键 API:
- 密封类:
LoginViewAction(登录页面操作定义)README.md:47-53 - 操作类型:
Login(登录)、ClearAccount(清空账号)、ClearPassword(清空密码)、UpdateAccount(更新账号)、UpdatePassword(更新密码)
关键数据结构:
kotlin1sealed class LoginViewAction { 2 object Login : LoginViewAction() // 登录操作 3 object ClearAccount : LoginViewAction() // 清空账号 4 object ClearPassword : LoginViewAction() // 清空密码 5 data class UpdateAccount(val account: String) : LoginViewAction() // 更新账号 6 data class UpdatePassword(val password: String) : LoginViewAction() // 更新密码 7}
交互方式:View 层用户操作触发后,包装为 ViewAction 发送给 ViewModel 层的 StateProcessor 处理 README.md:55-58。
事件处理模块
职责边界:处理一次性事件(如 Toast 提示、页面导航),避免状态回退导致事件重复触发。不管理持续性 UI 状态。
入口与关键 API:
- 密封类:
LoginViewEvent(一次性事件定义)README.md:39-42 - 事件类型:
PopBack(返回上一页)、ErrorMessage(错误提示)
关键数据结构:
kotlin1sealed class LoginViewEvent { 2 object PopBack : LoginViewEvent() // 页面返回事件 3 data class ErrorMessage(val message: String) : LoginViewEvent() // 错误消息事件 4}
错误处理与边界条件:
- 事件消费后自动移除,避免配置变更(如屏幕旋转)后重复触发
- 使用
SingleLiveEvent或SharedFlow确保事件仅被消费一次
视图层模块
职责边界:作为 UI 承载单元(Activity/Composable),订阅 ViewState 变化实现界面刷新。不包含业务逻辑,不直接调用 ViewModel 方法。
入口与关键 API:
- Composable 函数:页面级 UI 组件
- 订阅方式:
collectAsState()订阅 ViewState
交互方式:
- 订阅 ViewState 获取页面所有状态 README.md:22
- 用户操作包装为 ViewAction 发送给 ViewModel
- 接收 ViewEvent 处理一次性事件
数据流与调用链
用户登录完整数据流
以下时序图展示了用户点击登录按钮后的完整数据流:
正在加载图表渲染器...
数据流要点说明:
- Intent 驱动:所有用户操作必须包装为 ViewAction,通过 Intent 机制驱动状态变更 README.md:23
- 单向流动:数据流方向为 View → Intent → Processor → State → View,形成闭环
- 状态优先:ViewState 变化自动触发 UI 重组,无需手动更新 UI
- 事件分离:一次性事件通过 ViewEvent 处理,避免状态回退导致重复触发
状态更新调用链
正在加载图表渲染器...
调用链要点说明:
- 不可变状态:使用
data class的copy()方法创建新状态实例,保证状态不可变性 README.md:30-34 - Reducer 模式:通过纯函数计算新状态,输入当前状态 + Action,输出新状态
- 响应式更新:Compose 自动检测状态变化,仅重组受影响的 UI 组件
核心设计决策与取舍
1. 选择 MVI 而非 MVVM
决策理由:
- MVI 的单向数据流与 Compose 声明式 UI 思想高度契合 README.md:64
- ViewState 集中管理减少模板代码,只需订阅一个 State 即可获取页面所有状态 README.md:62
- Intent 机制约束 View 与 ViewModel 交互方式,降低耦合度 README.md:61
已知限制:
- 简单页面可能引入过多模板代码(ViewState、ViewAction、ViewEvent 定义)
- 状态集中管理可能导致 ViewState 类过大,需要合理拆分
2. 使用密封类定义 Intent 和 Event
决策理由:
- 类型安全:编译期检查所有可能的操作/事件类型
- 穷尽匹配:
when表达式强制处理所有分支,避免遗漏 - 可扩展:新增操作/事件只需添加子类,不影响现有代码
关键实现:sealed class LoginViewAction 和 sealed class LoginViewEvent README.md:39-53
3. ViewState 使用 data class
决策理由:
- 自动生成
copy()方法,支持不可变状态更新 - 自动生成
equals()/hashCode(),便于状态比较和去重 - 解构声明支持,便于提取状态字段
关键实现:data class LoginViewState README.md:30-34
4. 分离持续性状态与一次性事件
决策理由:
- ViewState 管理持续性状态(如文本内容、加载状态)
- ViewEvent 处理一次性事件(如 Toast、页面跳转)
- 避免配置变更后事件重复触发
关键实现:sealed class LoginViewEvent README.md:39-42
5. ViewModel 实现对 View 层屏蔽
决策理由:
- View 层只能通过发送 Intent 驱动事件,不能直接调用 ViewModel 方法 README.md:61
- 降低 View 与 ViewModel 耦合度,便于单元测试
- 统一交互入口,便于添加日志、监控等横切关注点
技术选型
| 技术 | 用途 | 选型理由 | 替代方案 |
|---|---|---|---|
| MVI 架构 | 应用架构模式 | 单向数据流与 Compose 声明式 UI 契合,状态集中管理减少模板代码 | MVVM、MVP |
| Jetpack Compose | UI 框架 | 声明式 UI,与 MVI 架构思想一致,减少 UI 样板代码 | XML + View System |
| Kotlin Sealed Class | Intent/Event 定义 | 类型安全,编译期检查,穷尽匹配 | Enum、普通类 |
| Kotlin Data Class | ViewState 定义 | 自动生成 copy/equals/hashCode,支持不可变状态 | 普通类 + 手动实现 |
| StateFlow | 状态流管理 | 热流,支持多订阅者,与 Compose collectAsState 无缝集成 | LiveData、RxJava |
| SharedFlow | 一次性事件流 | 支持配置 replay=0 避免事件重复消费 | LiveData、Channel |
| Hilt | 依赖注入 | 编译期生成代码,性能优于运行时反射 | Koin、Dagger2 |
| Retrofit | 网络请求 | 类型安全的 HTTP 客户端,支持 Kotlin 协程 | Ktor、OkHttp |
模块依赖关系
正在加载图表渲染器...
依赖关系要点说明:
- 单向依赖:UI 层依赖 MVI 架构层,MVI 层依赖 ViewModel 层,ViewModel 层依赖数据层,依赖方向清晰
- MVI 组件解耦:ViewState、ViewAction、ViewEvent 相互独立,通过 ViewModel 中的 StateProcessor 协调
- 数据层抽象:Repository 屏蔽数据来源(API/DB),ViewModel 只依赖 Repository 接口
关键配置与启动流程
MVI 核心组件配置
ViewState 定义规范 README.md:30-34:
- 使用
data class定义页面所有状态 - 为所有字段提供默认值,避免空指针异常
- 状态字段应包含 UI 显示所需的所有数据
ViewAction 定义规范 README.md:47-53:
- 使用
sealed class定义所有用户操作 object用于无参数操作,data class用于带参数操作- 操作命名应清晰表达用户意图
ViewEvent 定义规范 README.md:39-42:
- 使用
sealed class定义所有一次性事件 - 仅包含需要消费一次的事件(如 Toast、导航)
- 持续性状态应放在 ViewState 中管理
启动流程
- 初始化 ViewState:ViewModel 创建时初始化默认状态
- 订阅状态:Composable 通过
collectAsState()订阅 ViewState - 监听操作:Composable 用户操作触发 ViewAction 发送
- 处理操作:ViewModel 接收 ViewAction,调用 StateProcessor 处理
- 更新状态:StateProcessor 计算新状态,发射新 ViewState
- UI 重组:Compose 检测状态变化,自动重组受影响的 UI 组件
架构选型建议
MVI 与 Compose 的声明式 UI 思想高度契合,理论上应该是 Compose 的最佳伴侣 README.md:64。但 MVI 也只是在 MVVM 基础上做了一定改良,MVVM 也可以很好地配合 Compose 使用,开发者可根据项目需要选择合适的架构 README.md:65。
