4. 支付方式选择组件提升:选择状态提升、选项列表管理、选型回调更新
4.1 原理:为何要将选择状态提升?
支付流程中,支付方式的选择(如信用卡、支付宝、微信)通常会联动后续的输入区域、费率展示、验证逻辑。若选择状态被封装在子组件内部,父组件将无法感知用户选择,导致无法响应变化。通过 State Hoisting 将选择状态提升至父组件,可实现:
- 单一可信源:父组件持有状态,子组件仅负责展示和反馈。
- 可复用性:
PaymentMethodSelector成为纯 UI 组件,可在不同页面复用。 - 可测试性:无需模拟内部状态,直接注入参数即可测试回调。
- 联动能力:父组件可根据选中项动态更新后续视图(如显示信用卡输入框)。
4.2 实现流程
- 定义选项列表:在 ViewModel 或父组件中声明
List<PaymentMethod>。 - 声明状态:父组件通过
remember { mutableStateOf(defaultMethod) }持有选中项。 - 向下传递:将
selectedMethod和options作为参数传入PaymentMethodSelector。 - 回调更新:子组件检测到用户点击时,调用
onSelect(method)通知父组件。 - 父组件更新:父组件在回调 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 状态提升结构图
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值验证子组件显示正确,并使用onSelectmock 验证父组件是否收到回调。