4. 状态提升的层级决策:同层提升、父层提升、跨模块提升的权衡

状态提升(State Hoisting)是 Compose 组件间共享状态的基石,但“提升到哪一层”往往决定模块的独立性、复用性和可测试性。常见的决策点落在三个层级:同层(兄弟组件间)、父层(直接父组件)和跨模块(不同模块间)。本章围绕模块边界,系统对比三种模式的适用场景、数据流方向、控制流职责以及实际落地时的权衡。

4.1 三种提升模式对比

提升模式 状态存放位置 典型交互对象 模块耦合度 复用性影响 测试难度
同层提升 兄弟组件共享的 ViewModel 或回调持有者 同级兄弟组件 低(同一模块内) 组件可独立替换 中等(需 Mock 共享依赖)
父层提升 直接父 Composable 或父 ViewModel 父子组件 中(依赖父组件签名) 子组件可复用,父组件膨胀 低(父组件可隔离测试)
跨模块提升 外部模块的单例、导航参数、事件总线 不同模块的入口点 高(模块间硬依赖) 组件必须匹配外部协议 高(需模拟跨模块环境)

4.2 同层提升:兄弟间的协作模式

当两个组件在同一个父级作用域下且地位对等时,通常采用共享 ViewModel 或回调接口来提升状态。典型场景:搜索栏与结果列表共用同一个查询关键词。

  • 数据流:状态在 ViewModel 中定义,兄弟组件通过 ViewModel 暴露的 StateFlow 读取、通过方法写入。
  • 控制流:事件由兄弟组件触发,直接调用 ViewModel 的方法,无需经过父组件。
  • 优缺点:降低父组件责任,但要求兄弟组件在同一个 ViewModel 作用域内(如 NavBackStackEntry 持有的 ViewModelStore)。

典型代码结构(同层提升)

class SharedViewModel : ViewModel() {
    private val _query = MutableStateFlow("")
    val query: StateFlow<String> = _query.asStateFlow()
    fun updateQuery(newQuery: String) { _query.value = newQuery }
}

@Composable
fun SearchScreen(viewModel: SharedViewModel = viewModel()) {
    Column {
        SearchBar(onQueryChange = { viewModel.updateQuery(it) })
        SearchResults(query = viewModel.query.collectAsState().value)
    }
}

4.3 父层提升:标准单向数据流

这是 Compose 官方推荐的主流模式:父组件持有状态,通过参数传递给子组件,子组件通过回调通知父组件更新。适用于父子组件间强耦合但希望子组件纯渲染的场景。

  • 数据流:状态由父组件管理(如 mutableStateOf 或 ViewModel),以参数形式流入子组件。
  • 控制流:事件通过命名规范的回调(如 onValueChange)向上冒泡。
  • 利弊:子组件可完全无状态、可复用;但父组件因承担太多状态管理责任而容易膨胀,成为“上帝组件”。

警惕“父层提升过度”

当父组件需要同时管理多个子组件的不相关状态时,应拆分为多个独立的提升点,或使用子 ViewModel 进行同层提升,避免父组件承担过多业务逻辑。

4.4 跨模块提升:突破模块边界的代价

当状态需要跨越 Module 边界(如从用户模块传递到订单模块)时,必须通过外部机制(如 Application 级别的单例、导航路由参数、事件总线或跨模块导航参数)。该模式常用于全局配置、用户登录态、导航参数等场景。

  • 数据流:状态由某个公共模块持有,其他模块通过依赖注入或路由参数获取。
  • 控制流:事件通过路由拦截、全局回调或广播触发。
  • 风险点:跨模块硬依赖破坏模块化;状态生命周期与模块不匹配(如 Activity 重建后状态丢失)。需要通过接口抽象(如 Repository 接口)来解耦。

跨模块提升的推荐做法

使用依赖注入(Hilt / Koin)提供接口级别的状态源,模块内组件只依赖抽象接口,不直接引用具体实现。例如将用户状态定义在 UserRepository 接口中,订单模块通过 DI 注入该接口来读取用户信息。

4.5 层级决策模型

下图总结了三种提升模式的决策路径,帮助你按模块边界、组件职责和复用需求快速定位合适的提升层级。

状态提升层级决策模型 同层提升 ComponentA ComponentB Shared ViewModel 父层提升 ParentComponent (状态持有者) ChildA ChildB 事件↑ 事件↑ 跨模块提升 ModuleA ModuleB External Store (单例/路由参数) 事件→ 选择原则:优先尝试同层提升(低耦合),必要时使用父层提升(标准模式),跨模块提升作为最后手段并做好接口抽象。

4.6 落地步骤

  1. 识别模块边界:确认需要通信的组件是否在同一个模块内、同层级、父子关系或跨模块。
  2. 评估复用需求:子组件是否需要被多个不同父组件复用?如果是,优先考虑同层提升以避免父组件膨胀。
  3. 编写状态契约:明确状态读/写接口,用 StateFlow 或 snapshotFlow 封装,避免直接暴露 mutable 状态。
  4. 选择提升模式:按模块边界从低到高尝试——同层→父层→跨模块。
  5. 实现并测试:为每个提升点编写单元测试(ViewModel 或纯函数),验证数据流与控制流一致性。

关键验收标准

  • 模块职责单一:父组件不应含有与自身业务无关的状态。
  • 数据流单向:状态从上往下传递,事件从下往上提交,无循环依赖。
  • 无冗余状态:同一数据在系统中仅存在一份状态源。
  • 测试覆盖:每个提升点可独立 Mock 并验证单向数据流。

4.7 方案权衡速查

决策因素 同层提升 父层提升 跨模块提升
组件复用性 高(组件间解耦) 中(子组件可复用,父组件耦合) 低(依赖外部协议)
父组件复杂度 低(状态分散到 ViewModel) 高(父组件膨胀风险) 低(状态外部化)
状态生命周期管理 由 ViewModel 管理,可随导航生命周期 由 Composable 或 ViewModel 管理 需与外部模块生命周期对齐(如 Application)
跨模块迁移成本 低(模块内重构) 低(模块内重构) 高(需修改 DI 配置和接口)
推荐场景 兄弟组件频繁通讯(如搜索、筛选) 标准表单、Detail 页面 用户登录态、导航参数、全局配置

在实际项目中,你几乎总是先从父层提升开始,当发现父组件过于臃肿时,将部分状态下放至兄弟共享 ViewModel(同层提升),仅在跨模块场景下谨慎使用跨模式。掌握这三个层级的权衡,就能在模块边界清晰的前提下,设计出高内聚、低耦合的状态管理架构。