4. 支付方式选择组件提升:选择状态提升、选项列表管理、选型回调更新

4.1 原理:为何要将选择状态提升?

支付流程中,支付方式的选择(如信用卡、支付宝、微信)通常会联动后续的输入区域、费率展示、验证逻辑。若选择状态被封装在子组件内部,父组件将无法感知用户选择,导致无法响应变化。通过 State Hoisting 将选择状态提升至父组件,可实现:

  • 单一可信源:父组件持有状态,子组件仅负责展示和反馈。
  • 可复用性:PaymentMethodSelector 成为纯 UI 组件,可在不同页面复用。
  • 可测试性:无需模拟内部状态,直接注入参数即可测试回调。
  • 联动能力:父组件可根据选中项动态更新后续视图(如显示信用卡输入框)。

4.2 实现流程

  1. 定义选项列表:在 ViewModel 或父组件中声明 List<PaymentMethod>。
  2. 声明状态:父组件通过 remember { mutableStateOf(defaultMethod) } 持有选中项。
  3. 向下传递:将 selectedMethod 和 options 作为参数传入 PaymentMethodSelector。
  4. 回调更新:子组件检测到用户点击时,调用 onSelect(method) 通知父组件。
  5. 父组件更新:父组件在回调 lambda 中更新 selectedMethod 状态。
关键约定:子组件参数中避免出现 var 类型的状态,只接收 val 和回调。状态提升后,子组件内部不可直接修改选中值。

4.3 代码实现:无状态 PaymentMethodSelector

@Composable
fun PaymentMethodSelector(
    options: List<PaymentMethod>,
    selected: PaymentMethod,
    onSelect: (PaymentMethod) -> Unit
) {
    Column(modifier = Modifier.fillMaxWidth()) {
        options.forEach { method ->
            Row(
                modifier = Modifier
                    .fillMaxWidth()
                    .clickable { onSelect(method) }
                    .padding(horizontal = 16.dp, vertical = 12.dp),
                verticalAlignment = Alignment.CenterVertically
            ) {
                RadioButton(
                    selected = method == selected,
                    onClick = { onSelect(method) }
                )
                Spacer(modifier = Modifier.width(12.dp))
                Text(text = method.label, style = MaterialTheme.typography.bodyLarge)
            }
        }
    }
}
// 父组件(支付页面)
@Composable
fun PaymentPage(viewModel: PaymentViewModel = viewModel()) {
    val options by viewModel.paymentMethods.observeAsState(initial = emptyList())
    var selectedMethod by remember { mutableStateOf(options.firstOrNull() ?: PaymentMethod.NONE) }

    PaymentMethodSelector(
        options = options,
        selected = selectedMethod,
        onSelect = { method -> selectedMethod = method }
    )
    // 根据 selectedMethod 渲染额外输入区域...
}

4.4 状态提升结构图

支付方式选择组件状态提升流程 父组件 (持有状态) PaymentMethodSelector (无状态) selectedMethod (mutableStateOf) options 列表 (来自 ViewModel) onSelect 回调 传递 selected + options

4.5 调试要点

  • Compose Inspector:检查重组范围,确保 PaymentMethodSelector 的 recomposition 由父组件状态变化驱动,而非内部变量。
  • 日志验证:在 onSelect 回调中打印日志,确认回调被及时触发且参数正确。
  • 状态一致性:使用 snapshotFlow 观察 selectedMethod 变化,确保父组件后续 UI 正确联动。
// 调试辅助:观察状态变化
@Composable
fun PaymentPage() {
    var selectedMethod by remember { mutableStateOf(defaultMethod) }
    LaunchedEffect(selectedMethod) {
        Log.d("Payment", "Payment method changed to: ${selectedMethod.name}")
    }
    // ...
}

4.6 常见错误与规避

错误现象正确做法
子组件内部使用 remember { mutableStateOf() }父组件无法感知选择变化,UI 不同步状态全部由父组件持有,子组件只接收 val 和回调
回调中忘记更新状态(如直接调用 API 后不赋值)UI 点击无反应确保 onSelect 内执行状态赋值
选项列表 options 使用 remember 而非 derivedStateOf列表变化时子组件不重绘将选项列表作为参数传递,父组件保证稳定引用或使用 key
在 ViewModel 中直接暴露 MutableState<PaymentMethod>导致 ViewModel 持有可写状态,违背单向数据流ViewModel 仅提供不可变 Flow/State,父组件使用 mutableStateOf

4.7 性能与稳定性优化

  • 使用 derivedStateOf:若选中项由列表计算得出(如根据可用性过滤),用 derivedStateOf 避免不必要重组。
  • 稳定参数:确保 options 引用稳定或使用 remember 包装,防止子组件频繁重组。
  • 避免在回调中执行耗时任务:onSelect 仅做状态更新,网络请求等应通过 ViewModel 的协程处理。
  • 测试单向数据流:编写 UI 测试时,直接提供不同 selected 值验证子组件显示正确,并使用 onSelect mock 验证父组件是否收到回调。