4. 支付金额展示组件

本章聚焦于支付页面中最核心的视觉元素——金额展示。我们将基于 State Hoisting 原则,构建一个只读、不可变的金额展示组件,支持货币符号自适应、千分位格式化,且其状态完全由父组件提升管理。组件不维护任何可变内部状态,保证数据流单向、稳定。

4.1 原理:不可变状态与展示分离

在 Compose 中,Text 等展示组件天生适合“读”操作。为支付金额组件设计不可变状态,意味着金额值只在 父组件 中作为 State 持有,通过回调或参数传入子组件。这种分离带来三大收益:

  • 可测试性:金额格式化逻辑独立,可单独验证。
  • 可复用性:同一组件可同时处理美元、人民币、欧元。
  • 性能:无需为展示组件分配额外 mutableStateOf,重绘范围最小化。
核心思想:金额展示组件应是“纯函数式”Composable——给定金额原始值和货币符号,返回格式化后的文本。

4.2 实现流程

  1. 定义不可变状态接口:父组件通过参数传递 amountValue: Long?(以分为单位)或 amountString: String?,避免组件自行持有可变状态。
  2. 货币符号注入:使用 java.util.Currency 或本地化资源 stringResource 获取符号。
  3. 千分位格式化:利用 java.text.NumberFormat 的 getCurrencyInstance(locale) 自动处理分组和符号位置。
  4. 组件内部纯转换:在 remember 块内计算格式化结果,依赖键为原始值 + 货币代码。
  5. 展示:将 AnnotatedString 或简单 String 传给 Text。

4.3 核心代码:PaymentAmountDisplay 组件

@Composable
fun PaymentAmountDisplay(
    amountInCents: Long?,
    currencyCode: String = "USD",
    modifier: Modifier = Modifier
) {
    val formattedAmount = remember(amountInCents, currencyCode) {
        if (amountInCents == null) return@remember null
        try {
            val locale = when (currencyCode) {
                "USD" -> Locale.US
                "CNY" -> Locale.CHINA
                "EUR" -> Locale.GERMANY
                else -> Locale.getDefault()
            }
            val format = NumberFormat.getCurrencyInstance(locale)
            format.maximumFractionDigits = 2
            format.minimumFractionDigits = 2
            format.format(amountInCents / 100.0)
        } catch (e: Exception) {
            "¥${amountInCents / 100.0}"
        }
    }

    Text(
        text = formattedAmount ?: "—",
        style = MaterialTheme.typography.h4,
        modifier = modifier
    )
}

上述组件:

  • 不声明任何 mutableStateOf,状态由参数 amountInCents 和 currencyCode 承载。
  • 使用 remember(…) 缓存格式化结果,仅在依赖变化时重算。
  • 异常时降级为简单格式(如 ¥123.45),保证健壮性。

4.4 货币符号与千分位格式化细节

货币代码符号示例千分位示例Locale
USD$$12,345.67Locale.US
CNY¥¥12,345.67Locale.CHINA
EUR€€12.345,67Locale.GERMANY

通过 NumberFormat.getCurrencyInstance(locale) 自动处理符号位置、千分位分隔符和小数点符号。若需要高度定制,也可手动使用 DecimalFormat 加货币符号拼接,但推荐内置方案以保持国际化一致性。

4.5 调试与部署注意事项

  • 测试浮点精度:金额传入统一为 Long(分),避免 Double 精度丢失;格式化时除以100.0。
  • RTL 支持:某些语言(如阿拉伯语)货币符号在右侧,NumberFormat 自动处理。
  • 空安全:amountInCents 为 null 时展示占位符(如“—”)。
  • 预览验证:使用 @Preview 注解传入不同货币和金额,快速确认 UI 表现。

4.6 常见错误

错误 1:在展示组件内部修改金额状态
现象:金额字段使用 var amount by remember { mutableStateOf(0L) },然后通过 TextField 编辑。正确做法:展示组件应为只读,编辑行为应提升至父组件。
错误 2:千分位格式化在重组中重复计算
现象:未使用 remember,导致每次重组都调用 NumberFormat.format()。改进:将格式化逻辑放入 remember(amount, currencyCode)。
错误 3:货币符号硬编码
现象:直接写 "$" 或 "¥"。改进:通过 Currency.getInstance(currencyCode).symbol 获取本地化符号。

4.7 性能与稳定性优化

  • 对象复用:NumberFormat 实例创建开销较大,可考虑在 remember 中缓存每个 Locale 对应的 NumberFormat。
  • 避免不必要的重组:父组件应使用 derivedStateOf 或 remember 只传递格式化后的 formattedAmount 给展示组件,减少内部计算。
  • 国际化懒加载:若应用支持几十种货币,可建立 HashMap<String, NumberFormat> 在 ViewModel 中一次性初始化。

4.8 状态提升架构图

支付金额展示组件——State Hoisting 架构 父 Composable State: amountInCents (不可变 Long?) PaymentAmountDisplay remember(amount, currencyCode) NumberFormat 格式化 货币符号+千分位 Text 显示金额 说明:金额展示组件不创建可变状态,所有数据由父组件通过参数传递;格式化通过 remember 缓存依赖。 货币符号由 Currency 对象或 Locale 决定,千分位由 NumberFormat 自动处理。
实战启示:将金额展示组件设计为“纯展示 + 不可变参数”,不仅匹配 State Hoisting 原则,还能在后续添加动画(如价格跳动)时保持状态源单一,避免副作用。