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 演进的唯一正确方式。**


相关推荐
-SOLO-3 小时前
Android Event 日志完全指南
android
壮哥_icon4 小时前
【Android 系统开发】从掉帧卡顿到丝滑:高频硬件中断(IRQ)的 CPU 亲和性性能优化实战
android·性能优化
千里马学框架4 小时前
Android Framework 新手学习踩坑建议指南
android·智能手机·性能优化·framework·aaos·车载开发·系统工程师
我命由我123456 小时前
执行 Gradle 指令报错,无法将“grep”项识别为 cmdlet、函数、脚本文件或可运行程序的名称
android·java·java-ee·android studio·android jetpack·android-studio·android runtime
狂奔solar7 小时前
ARMA 模型:从白噪声到万能逼近器
android
渣渣灰飞8 小时前
MySQL 系统学习 第五阶段:企业级 MySQL 实战开发 第二章:RBAC 权限系统设计
android·学习·mysql
qq_422828628 小时前
android 图形学之图层数据送显及合成图层(七)
android
MiyamuraMiyako9 小时前
Compose:从自动滚动到双锚点 LazyLayout
android·android jetpack
用户634214199350711 小时前
base_logger_core --- 跨平台 C++ 日志核心库
android
苦瓜花11 小时前
【Kotlin】初入门
android·开发语言·kotlin