State Hoosting 核心原理

State Hoisting(状态提升)是 Jetpack Compose 中管理 UI 状态的核心模式。它将可组合函数内部的状态“提升”到调用方,使得组件成为无状态(stateless)的纯展示单元,从而实现状态与 UI 的解耦、复用和可测试。

状态提升定义

状态提升的本质是将 remember 管理的可变状态从子组件中移动到父组件(或 ViewModel),子组件只接收不可变的状态值和一个事件回调(lambda),不直接持有或修改状态。

核心公式:子组件 = f(state: T, onEvent: (Event) -> Unit)

维度 有状态组件 无状态组件(提升后)
状态来源 内部 remember var 外部参数 value
修改状态 直接赋值 回调 onValueChange
可复用性 低(绑定内部逻辑) 高(可被不同状态源驱动)
测试难度 难(需模拟 Composable) 易(纯函数测试)

单向数据流(UDF)

State Hoisting 天然遵循单向数据流(Unidirectional Data Flow)原则:

  1. 事件(Event):用户操作(点击、输入)从 UI 向上传递。
  2. 状态(State):组件状态从上层(ViewModel / 父 Composable)向下传递。
  3. 数据不变性:子组件不能修改状态,只能通过回调上报事件。
State Hoisting + 单向数据流 ViewModel 父 Composable 子 Composable 用户事件 (点击/输入) onEvent state onEvent state 状态只能向下传递,事件只能向上传递 —— 形成不可逆的单向环

提升层次选择

状态应该提升到“最需要它的最近共同父级”。支付流程中常见层次选择规则:

  • 局部 UI 状态(如输入框文本):提升到当前屏幕的 Composable 内,使用 remember 就地管理。
  • 跨组件状态(如支付金额、商品清单):提升到屏幕级 ViewModel 中。
  • 全局状态(如用户会话、支付 ):使用 ViewModel + 依赖注入,或 Compose 的 CompositionLocal 但慎用。
状态范围 提升至 生命周期
单组件内部 组件自身 remember 组件重组期间
同屏幕共享 屏幕级 ViewModel 屏幕可见期间
跨屏幕/全局 单例 ViewModel / DI 容器 应用进程生命周期

支付场景经验:支付表单中的金额、卡号等字段应提升到表单 ViewModel,而“提交按钮是否可点击”这种派生状态可在 ViewModel 中通过 derivedStateOf 计算。

与 ViewModel 的关系

ViewModel 是状态提升的天然承载者。在 Compose 中,ViewModel 通过 viewModel() 函数获取,其内部使用 StateFlow 或 MutableState 暴露状态,Composable 通过 collectAsState() 转为 Compose State。

典型代码模式:

// ViewModel
class PaymentViewModel : ViewModel() {
    private val _amount = mutableStateOf("")
    val amount: State<String> = _amount

    fun onAmountChanged(new: String) {
        // 校验、格式化等业务逻辑
        _amount.value = new
    }

    val isSubmitEnabled: State<Boolean> = derivedStateOf {
        amount.value.isNotBlank() && amount.value.toDoubleOrNull() != null
    }
}

// Composable 层
@Composable
fun PaymentScreen(viewModel: PaymentViewModel = viewModel()) {
    val amount by viewModel.amount
    val enabled by viewModel.isSubmitEnabled
    AmountInputField(value = amount, onValueChange = viewModel::onAmountChanged)
    SubmitButton(enabled = enabled)
}

@Composable
fun AmountInputField(value: String, onValueChange: (String) -> Unit) {
    TextField(value = value, onValueChange = onValueChange)
}

@Composable
fun SubmitButton(enabled: Boolean) {
    Button(onClick = { /* 提交 */ }, enabled = enabled) {
        Text("提交支付")
    }
}

调试与常见错误

  • 错误:子组件内部使用 remember 并直接修改状态,导致父子状态不一致。应遵循“状态由父级提供,事件回调上报”。
  • 调试技巧:在 ViewModel 中记录 SnapshotStateObserver 或使用 Android Studio 的 Layout Inspector 查看重组原因。
  • 性能注意:状态提升到过高层次会导致不必要的重组,应保持提升的“最小必要层次”。

避坑:不要在子组件中同时持有状态和回调,导致双重事实来源(multiple sources of truth)。支付金额等敏感状态尤其需要唯一来源。

掌握 State Hoisting 的核心原理,是构建稳定、可复用支付流程 UI 组件的基础。下一节将深入具体支付表单的状态提升实现。