Android MVI 中 setState(reduce: State.() -> State) 设计说明文档


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)
问题:
  1. ❌ 无法保证 state 基于最新的 currentState
  2. ❌ 状态可能在外部被提前计算,存在并发风险
  3. ❌ reducer 逻辑分散,破坏 MVI 的一致性
  4. ❌ 无法保证 State 的"演进关系"

3.3 正确模型(✅)

text 复制代码
NewState = reduce(OldState)

而不是:

text 复制代码
NewState = 某个外部算好的 State

4. Lambda 什么时候执行?(关键理解点)

调用代码:

kotlin 复制代码
setState {
    copy(loadState = LoadState.Loading)
}

执行时序:

  1. { copy(...) }

    👉 只是创建了一个 Lambda(函数值),不会执行

  2. 进入 setState

  3. 执行这一行:

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)
}
问题:
  1. ❌ 其他字段会被重置为默认值 / null
  2. ❌ 破坏 State 的连续性
  3. ❌ UI 会出现"莫名其妙回退 / 闪烁"的 bug
  4. ❌ 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 演进的唯一正确方式。**


相关推荐
爱装代码的小瓶子2 小时前
【C++与Linux基础】进程间通讯方式:匿名管道
android·c++·后端
兴趣使然HX3 小时前
Android绘帧流程解析
android
JMchen1234 小时前
Android UDP编程:实现高效实时通信的全面指南
android·经验分享·网络协议·udp·kotlin
黄林晴4 小时前
Android 17 再曝猛料:通知栏和快捷设置终于分家了,这操作等了十年
android
有位神秘人5 小时前
Android获取设备中本地音频
android·音视频
JMchen1235 小时前
Android网络安全实战:从HTTPS到双向认证
android·经验分享·网络协议·安全·web安全·https·kotlin
CS创新实验室5 小时前
Pandas 3 的新功能
android·ide·pandas
ujainu5 小时前
护眼又美观:Flutter + OpenHarmony 鸿蒙记事本一键切换夜间模式(四)
android·flutter·harmonyos
三少爷的鞋5 小时前
为什么我不在 Android ViewModel 中直接处理异常?
android
草莓熊Lotso6 小时前
Linux 文件描述符与重定向实战:从原理到 minishell 实现
android·linux·运维·服务器·数据库·c++·人工智能