第2章 可组合函数的状态容器设计:remember与rememberSaveable的选择、ViewModel与Compose的边界
在Compose中,状态容器直接决定了组件的生存期、跨重组保持能力以及模块边界。选错容器会导致状态丢失、测试困难或架构耦合。本章从模块边界视角切入,厘清remember、rememberSaveable与ViewModel的职责分界,并提供可落地的选择决策树。
| 维度 | remember | rememberSaveable | ViewModel |
|---|---|---|---|
| 作用域 | 单次组合(重组存活) | 单次组合 + 进程重建存活 | Compose宿主(Activity/Fragment)生命周期 |
| 序列化要求 | 无 | 需实现Parcelable/Serializable或自定义Saver | 无(由架构层管理) |
| 模块边界 | 纯UI局部状态 | 可恢复的UI局部状态 | 业务逻辑与跨界面状态 |
| 测试友好度 | 中等(需ComposeTestRule) | 中等 | 高(纯Kotlin单元测试) |
| 典型场景 | 开关、折叠、展开 | 表单输入、滑动位置 | 列表数据、登录状态、API请求 |
决策步骤:从UI局部到业务边界
- 判断状态是否属于纯UI外观(动画进度、选择器展开)→
remember。 - 判断是否需要在进程重建后恢复(配置变更、低内存回收)→
rememberSaveable。 - 判断状态是否关联业务逻辑(网络请求、数据库读写、跨界面共享)→
ViewModel。 - 检查状态提升层级:若状态需要被多个无关Composable共享,则提升至最近的Common
ViewModel或StateHolder。
边界防越界规则:严禁将
ViewModel的StateFlow直接传入仅需局部动画的remember容器内;反之,业务状态应避免通过rememberSaveable碎片化分布在多个Composable中。
方案权衡:使用成本与长期维护
- remember + 手动保存:在
onDispose中序列化状态到持久化存储,适合极小数据量,但违反单一职责。 - rememberSaveable:零额外代码保存简单标量,但复杂对象需要自定义Saver,且不支持跨界面共享。
- ViewModel + StateFlow:适合有业务依赖的状态,但需额外引入Hilt/手动工厂,轻度UI状态过度工程化。
落地步骤
- 识别状态类别:根据上述决策树标记每个
var为UI局部/可恢复/业务。 - 统一Saver接口:对
rememberSaveable的复杂对象,创建ListSaver或MapSaver,放入数据模块。 - 定义Composable边界:将
ViewModel作为参数提升至屏幕级组件,子组件只接收展开状态和回调。 - 编写状态单元测试:对
ViewModel使用kotlinx-coroutines-test覆盖生命周期和输入变换。
验收标准
✓ 所有
✓ 没有任何一个
✓ 在不同屏幕旋转测试下,UI局部状态(如滚动位置)正确保持,业务状态不重复加载。
✓ 模块边界清晰:Composable内部无任何
✓ 所有
rememberSaveable的状态均能在进程重建后恢复,且恢复过程不触发额外网络请求。✓ 没有任何一个
ViewModel被直接用作remember的依赖(违反作用域直觉)。✓ 在不同屏幕旋转测试下,UI局部状态(如滚动位置)正确保持,业务状态不重复加载。
✓ 模块边界清晰:Composable内部无任何
viewModel()调用,仅通过参数传递状态与事件。
代码片段:自定义Saver示例
data class SearchQuery(val text: String, val filters: List<String>)
val SearchQuerySaver = listSaver<Saveable, SearchQuery>(
save = { listOf(it.text) + it.filters },
restore = { SearchQuery(it.first(), it.drop(1)) }
)
@Composable
fun SearchBar() {
var query by rememberSaveable(stateSaver = SearchQuerySaver) {
mutableStateOf(SearchQuery("", emptyList()))
}
// ... UI 使用 query
}
常见反模式:在
ViewModel内部使用rememberSaveable保存StateFlow的值——这会破坏ViewModel的测试性和协程作用域。应使用SavedStateHandle作为ViewModel的序列化入口。
数据流与控制流边界
数据流:状态从ViewModel单向流动到Composable,通过collectAsState()转译为Compose可观测状态。
控制流:用户事件通过lambda回调(如onSearch)从组合树底部向上传递,最终触发ViewModel中的业务方法。
关键边界:remember和rememberSaveable只应出现在事件回调的下游,不应拦截去往ViewModel的控制流。
模块职责清单
- Composable函数:仅消费状态、仅触发回调,不含业务逻辑。
- StateHolder(自定义):管理UI局部状态和恢复逻辑,可被Composable内嵌。
- ViewModel:持有业务状态、管理协程作用域、提供界面uiState。
- Saver:放置在数据层或ui-util模块,不依赖Compose。