第2章 可组合函数的状态容器设计:remember与rememberSaveable的选择、ViewModel与Compose的边界

在Compose中,状态容器直接决定了组件的生存期、跨重组保持能力以及模块边界。选错容器会导致状态丢失、测试困难或架构耦合。本章从模块边界视角切入,厘清remember、rememberSaveable与ViewModel的职责分界,并提供可落地的选择决策树。

维度rememberrememberSaveableViewModel
作用域单次组合(重组存活)单次组合 + 进程重建存活Compose宿主(Activity/Fragment)生命周期
序列化要求无需实现Parcelable/Serializable或自定义Saver无(由架构层管理)
模块边界纯UI局部状态可恢复的UI局部状态业务逻辑与跨界面状态
测试友好度中等(需ComposeTestRule)中等高(纯Kotlin单元测试)
典型场景开关、折叠、展开表单输入、滑动位置列表数据、登录状态、API请求

决策步骤:从UI局部到业务边界

  1. 判断状态是否属于纯UI外观(动画进度、选择器展开)→ remember。
  2. 判断是否需要在进程重建后恢复(配置变更、低内存回收)→ rememberSaveable。
  3. 判断状态是否关联业务逻辑(网络请求、数据库读写、跨界面共享)→ ViewModel。
  4. 检查状态提升层级:若状态需要被多个无关Composable共享,则提升至最近的Common ViewModel或StateHolder。
边界防越界规则:严禁将ViewModel的StateFlow直接传入仅需局部动画的remember容器内;反之,业务状态应避免通过rememberSaveable碎片化分布在多个Composable中。
状态容器选型决策树 状态来源 纯UI外观 / 动画? remember 需进程重建恢复? rememberSaveable 涉及业务逻辑? ViewModel 共有状态提升 提示:共有状态提升可通过外层Composable注入,或引入轻量级 StateHolder 代替 ViewModel

方案权衡:使用成本与长期维护

  • remember + 手动保存:在onDispose中序列化状态到持久化存储,适合极小数据量,但违反单一职责。
  • rememberSaveable:零额外代码保存简单标量,但复杂对象需要自定义Saver,且不支持跨界面共享。
  • ViewModel + StateFlow:适合有业务依赖的状态,但需额外引入Hilt/手动工厂,轻度UI状态过度工程化。

落地步骤

  1. 识别状态类别:根据上述决策树标记每个var为UI局部/可恢复/业务。
  2. 统一Saver接口:对rememberSaveable的复杂对象,创建ListSaver或MapSaver,放入数据模块。
  3. 定义Composable边界:将ViewModel作为参数提升至屏幕级组件,子组件只接收展开状态和回调。
  4. 编写状态单元测试:对ViewModel使用kotlinx-coroutines-test覆盖生命周期和输入变换。
验收标准
✓ 所有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。