3. 支付金额输入组件提升:输入状态提升到父级、输入校验回调、实时格式化显示

3.1 原理

在 Jetpack Compose 中,状态提升(State Hoisting) 是将可组合函数内部的状态“上移”到其调用者(父级),从而实现单向数据流:父级持有状态、子级通过回调触发更新。对于支付金额输入组件,提升的必要性尤为突出:

  • 多组件协同:金额值需要同步传递给总额计算、货币选择、提交按钮等兄弟组件。
  • 校验集中化:输入合法性(如金额上限、小数点位数)应由父级统一管控,避免子级各有校验逻辑导致不一致。
  • 格式化解耦:实时格式化(千分位、货币符号)由父级执行,子级仅负责原始输入事件。

核心模式:父级通过 value: String 向下传递输入文本,并通过 onValueChange: (String) -> Unit 回调接收子级的上报。

关键契约:子级组件不得持有自身输入状态,所有显示与控制完全依赖父级传入的参数。

3.2 实现流程

步骤操作说明
1在父级定义状态使用 remember { mutableStateOf("") } 保存原始输入字符串
2定义校验与格式化回调创建 validateAmount 和 formatAmount 函数,返回新字符串
3将状态与回调传入子级子级接收 value 和 onValueChange,仅调用 onValueChange 上报
4父级处理上报值在 onValueChange 中执行校验 → 格式化 → 更新 amount

3.3 核心代码实现

以下为父级 Composable(支付表单)与子级 AmountInput 的完整示例:

// 父级:持有关键状态并驱动子级
@Composable
fun PaymentForm() {
    var amount by remember { mutableStateOf("") }
    val validatedAmount by remember(amount) {
        derivedStateOf {
            val cleaned = amount.filter { it.isDigit() || it == '.' }
                .takeWhile { it != '.' || cleanedCount(it, '.') <= 1 }
            cleaned
        }
    }
    val displayAmount = formatWithCurrency(validatedAmount)

    AmountInput(
        value = displayAmount,
        onValueChange = { raw ->
            // 仅允许数字和小数点
            val filtered = raw.filter { it.isDigit() || it == '.' }
            if (filtered.length <= 12) {
                amount = filtered
            }
        }
    )
}

// 子级:纯展示,不保留任何状态
@Composable
fun AmountInput(
    value: String,
    onValueChange: (String) -> Unit
) {
    OutlinedTextField(
        value = value,
        onValueChange = onValueChange,
        label = { Text("支付金额") },
        keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Decimal),
        isError = value.isNotEmpty() && value.toDoubleOrNull() == null,
        singleLine = true
    )
}

// 格式化辅助
fun formatWithCurrency(raw: String): String {
    val decimal = raw.toDoubleOrNull() ?: return "¥0.00"
    return "¥%,.2f".format(decimal)
}
提示:derivedStateOf 确保校验与格式化仅在 amount 变化时重新计算,避免每次重组都触发昂贵操作。

3.4 实时格式化显示的两种策略

策略实现方式适用场景
父级拦截式在 onValueChange 中先校验再格式化,最后赋值给 amount需要严格限制输入格式(如禁止前缀空格)
子级展示式父级传递原始值,子级通过 VisualTransformation 实时渲染仅改变显示外观,不修改实际数据

3.5 常见错误与调试

  • 错误1:子级使用 remember 存储临时状态 → 导致输入丢失,父级无法感知中间值。
    ✅ 应完全依赖 value 参数。
  • 错误2:在 onValueChange 中直接做格式化 → 造成光标跳跃,用户输入体验差。
    ✅ 使用 VisualTransformation 保持光标位置。
  • 错误3:校验只放在子级 → 兄弟组件或提交逻辑无法获取校验结果。
    ✅ 所有校验逻辑提升到父级,并通过 isError 参数回传。
调试建议:开启 Compose 布局检察工具(Layout Inspector),观察 AmountInput 的 value 是否随父级状态同步变化。光标闪烁时优先检查 onValueChange 是否过度过滤导致重组。

3.6 性能与稳定性优化

  • 使用 derivedStateOf 避免不必要的格式化计算。
  • 限制输入长度(如 if (filtered.length <= 12))防止大字符串导致重组卡顿。
  • 将 formatWithCurrency 标记为 inline + @Stable,减少 lambda 重复创建。
  • 对于高频输入,考虑使用 snapshotFlow 或 debounce 降低格式刷新频率。
支付金额输入状态提升流程图 父级 PaymentForm 持有 amount State 校验回调 validate 限制小数位数、长度 格式化 formatWithCurrency 带千分位货币符号 子级 AmountInput 接收 value + onValueChange OutlinedTextField value (已格式化) onValueChange(raw) 更新 amount 单向数据流 状态由父级向下传递 事件由子级向上回调

3.7 部署与集成检查清单

  • 确认子级 AmountInput 不引用任何 remember 状态。
  • 测试键盘输入:小数点、退格、粘贴混合内容是否符合过滤逻辑。
  • 验证 derivedStateOf 的 key 是否与 amount 绑定。
  • 检查 formatWithCurrency 是否抛出异常(如空字符串转 toDouble)。
  • 使用 @Preview 注解确认组件在未输入与输入后的正确渲染。