Android MVI 中 setState(reduce: State.() -> State) 设计说明文档
1. 背景与设计目标
在 Android MVI(Model--View--Intent)架构中,State 是不可变的 ,UI 的任何变化都必须通过 State 的演进(reduction) 来完成。
本设计的核心目标是:
- 保证 State 更新是原子性的
- 确保 新 State 一定基于旧 State
- 避免 UI 状态被意外重置
- 让状态更新逻辑 集中、可预测、可追踪
2. 核心代码
kotlin
protected fun setState(reduce: State.() -> State) {
val newState = currentState.reduce()
newState.toString().iLog(TAG)
currentState = newState
_uiState.value = currentState
}
3. 为什么 setState 要接收 Lambda?
3.1 reduce 不是 State,而是「状态演进规则」
kotlin
State.() -> State
表示:
给我一个旧 State,我返回一个新 State
这不是一个值,而是一段 "如何从旧状态计算新状态的逻辑"。
3.2 为什么不能设计成这样?(❌ 错误)
kotlin
protected fun setState(state: State)
问题:
- ❌ 无法保证 state 基于最新的
currentState - ❌ 状态可能在外部被提前计算,存在并发风险
- ❌ reducer 逻辑分散,破坏 MVI 的一致性
- ❌ 无法保证 State 的"演进关系"
3.3 正确模型(✅)
text
NewState = reduce(OldState)
而不是:
text
NewState = 某个外部算好的 State
4. Lambda 什么时候执行?(关键理解点)
调用代码:
kotlin
setState {
copy(loadState = LoadState.Loading)
}
执行时序:
-
{ copy(...) }👉 只是创建了一个 Lambda(函数值),不会执行
-
进入
setState -
执行这一行:
kotlin
val newState = currentState.reduce()
👉 此时 Lambda 才真正执行
核心规则(必须记住)
Lambda 在"被传递时不会执行",
只有在调用()时才会执行
5. 为什么 reducer 里几乎总是用 copy?
5.1 State 的正确形态
kotlin
data class XxxState(
val loadState: LoadState = LoadState.Idle,
val loadMoreState: LoadMoreState = LoadMoreState.Idle,
val data: Data? = null
)
State 必须是:
data class- 不可变(val)
- 可整体替换
5.2 copy 的作用
kotlin
copy(loadState = LoadState.Loading)
含义是:
在旧 State 基础上,只修改指定字段,其余字段保持不变
等价于手写:
kotlin
XxxState(
loadState = LoadState.Loading,
loadMoreState = this.loadMoreState,
data = this.data
)
5.3 为什么不能 new 一个 State?(❌)
kotlin
setState {
XxxState(loadState = LoadState.Loading)
}
问题:
- ❌ 其他字段会被重置为默认值 / null
- ❌ 破坏 State 的连续性
- ❌ UI 会出现"莫名其妙回退 / 闪烁"的 bug
- ❌ reducer 语义被破坏(不再基于旧状态)
6. 为什么 State 常用「接口 + data class」?
kotlin
interface UiState
interface XxxState : UiState {
val loadState: LoadState
val data: Data?
}
data class XxxStateImpl(
override val loadState: LoadState = LoadState.Idle,
override val data: Data? = null
) : XxxState
好处:
- View 层依赖接口(解耦)
- 内部实现可演进
- reducer 中始终操作具体 data class(支持
copy)
7. 正确使用方式(推荐写法)
✅ 标准用法
kotlin
setState {
copy(loadState = LoadState.Loading)
}
kotlin
setState {
copy(
loadState = LoadState.LoadSuccess,
data = result
)
}
❌ 错误用法(禁止)
kotlin
setState {
XxxState(...)
}
kotlin
setState(
currentState.copy(...)
)
8. 注意事项(非常重要)
8.1 reducer 中不要有副作用
❌ 不要:
- 发请求
- 写数据库
- 打 Toast
- 启动协程
✅ reducer 只负责计算新 State
8.2 reducer 必须是"纯函数"
同样的输入 State,必须得到同样的输出 State。
8.3 不要在 reducer 外部修改 State
❌:
kotlin
currentState = currentState.copy(...)
✅:
kotlin
setState { copy(...) }
9. 一句话总结(可以放在文档最前面)
**
setState接收的不是 State,而是"如何从旧 State 生成新 State 的规则"。
copy是 MVI 中 State 演进的唯一正确方式。**