4. 支付金额展示组件
本章聚焦于支付页面中最核心的视觉元素——金额展示。我们将基于 State Hoisting 原则,构建一个只读、不可变的金额展示组件,支持货币符号自适应、千分位格式化,且其状态完全由父组件提升管理。组件不维护任何可变内部状态,保证数据流单向、稳定。
4.1 原理:不可变状态与展示分离
在 Compose 中,Text 等展示组件天生适合“读”操作。为支付金额组件设计不可变状态,意味着金额值只在 父组件 中作为 State 持有,通过回调或参数传入子组件。这种分离带来三大收益:
- 可测试性:金额格式化逻辑独立,可单独验证。
- 可复用性:同一组件可同时处理美元、人民币、欧元。
- 性能:无需为展示组件分配额外
mutableStateOf,重绘范围最小化。
核心思想:金额展示组件应是“纯函数式”Composable——给定金额原始值和货币符号,返回格式化后的文本。
4.2 实现流程
- 定义不可变状态接口:父组件通过参数传递
amountValue: Long?(以分为单位)或amountString: String?,避免组件自行持有可变状态。 - 货币符号注入:使用
java.util.Currency或本地化资源stringResource获取符号。 - 千分位格式化:利用
java.text.NumberFormat的getCurrencyInstance(locale)自动处理分组和符号位置。 - 组件内部纯转换:在
remember块内计算格式化结果,依赖键为原始值 + 货币代码。 - 展示:将
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.67 | Locale.US |
| CNY | ¥ | ¥12,345.67 | Locale.CHINA |
| EUR | € | €12.345,67 | Locale.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 原则,还能在后续添加动画(如价格跳动)时保持状态源单一,避免副作用。