# 鸿蒙 HarmonyOS 应用开发实战(第22期)|货币转换(Currency Converter)— Flex 布局与数据驱动精讲
一、应用概述
货币转换(Currency Converter) 是一款实用的汇率换算工具,支持人民币(CNY)、美元(USD)、欧元(EUR)、日元(JPY)、英镑(GBP)和韩元(KRW)六大主流货币之间的实时换算。用户只需输入金额并选择源货币与目标货币,即可获得精确的换算结果,同时支持一键互换货币对和快速切换源货币。应用基于 HarmonyOS 的 ArkTS 语言和声明式 UI 框架构建,完美展示了 Flex 弹性布局、Record 字典数据结构、双向数据流、以及 UI 状态驱动的货币选择等核心技术的综合运用。
对于鸿蒙开发者而言,货币转换是一个典型的数据驱动应用案例——它让你在约90行代码中,同时掌握字典查找、双向绑定、弹性换行布局三大关键能力,这些能力广泛适用于汇率、计算器、配置面板等数据展示与操作场景。
1.1 功能特性
- 六大货币支持:人民币、美元、欧元、日元、英镑、韩元
- 实时汇率换算:输入金额立即转换,无需点击确认按钮
- 货币符号显示:每种货币使用其标准符号(¥、$、€、£、₩)
- 一键互换功能:点击圆形按钮快速交换源货币和目标货币
- 汇率信息展示:显示当前使用的汇率值
- 货币快速选择:通过 Flex 布局的可点击标签切换源货币
- 货币名称展示:每种货币显示其中文名称,提升可读性
1.2 适用场景
- 出国旅游前的汇率查询
- 跨境电商购物比价
- 外汇投资参考
- 国际贸易成本计算
- 财务报告货币换算
二、技术架构
2.1 整体架构概览
┌─────────────────────────────────────────┐
│ 数据层 │
│ RATES (Record<string, Record<>>) 汇率表 │
│ CURRENCIES (string[]) 货币列表 │
│ SYMBOLS / NAMES 元数据字典 │
├─────────────────────────────────────────┤
│ 状态管理层 │
│ @State fromCurrency / toCurrency / │
│ amount / result │
├─────────────────────────────────────────┤
│ 业务逻辑层 │
│ convert() 汇率换算 + get rate() 汇率查询 │
├─────────────────────────────────────────┤
│ UI 表示层 │
│ 输入面板 / 互换按钮 / 结果面板 / │
│ 货币选择区 │
└─────────────────────────────────────────┘
2.2 核心数据结构设计
汇率数据使用嵌套的 Record 类型存储:
const RATES: Record<string, Record<string, number>> = {
'CNY': { 'USD': 0.14, 'EUR': 0.13, 'JPY': 21.5, 'GBP': 0.11, 'KRW': 190, 'CNY': 1 },
'USD': { 'CNY': 7.15, 'EUR': 0.92, 'JPY': 153, 'GBP': 0.79, 'KRW': 1360, 'USD': 1 },
'EUR': { 'CNY': 7.78, 'USD': 1.09, 'JPY': 166, 'GBP': 0.86, 'KRW': 1480, 'EUR': 1 },
'JPY': { 'CNY': 0.047, 'USD': 0.0065, 'EUR': 0.0060, 'GBP': 0.0052, 'KRW': 8.9, 'JPY': 1 },
};
Record<K, V> 是 ArkTS 中的字典类型,等价于 TypeScript 的 Record<K, V>。这里使用了两层嵌套:
- 第一层键为源货币代码
- 第二层键为目标货币代码
- 值为汇率(直接汇率,即1单位源货币可兑换的目标货币数)
这种设计实现了O(1) 时间复杂度的汇率查找,比数组遍历或 switch-case 语句高效得多。
辅助元数据同样使用 Record 存储:
const SYMBOLS: Record<string, string> = { 'CNY': '¥', 'USD': '$', 'EUR': '€', 'JPY': '¥', 'GBP': '£', 'KRW': '₩' };
const NAMES: Record<string, string> = { 'CNY': '人民币', 'USD': '美元', 'EUR': '欧元', 'JPY': '日元', 'GBP': '英镑', 'KRW': '韩元' };
三、核心代码深度分析
3.1 完整的页面源码
import { router } from '@kit.ArkUI';
const RATES: Record<string, Record<string, number>> = {
'CNY': { 'USD': 0.14, 'EUR': 0.13, 'JPY': 21.5, 'GBP': 0.11, 'KRW': 190, 'CNY': 1 },
'USD': { 'CNY': 7.15, 'EUR': 0.92, 'JPY': 153, 'GBP': 0.79, 'KRW': 1360, 'USD': 1 },
'EUR': { 'CNY': 7.78, 'USD': 1.09, 'JPY': 166, 'GBP': 0.86, 'KRW': 1480, 'EUR': 1 },
'JPY': { 'CNY': 0.047, 'USD': 0.0065, 'EUR': 0.0060, 'GBP': 0.0052, 'KRW': 8.9, 'JPY': 1 },
};
const CURRENCIES: string[] = ['CNY', 'USD', 'EUR', 'JPY', 'GBP', 'KRW'];
const SYMBOLS: Record<string, string> = { 'CNY': '¥', 'USD': '$', 'EUR': '€', 'JPY': '¥', 'GBP': '£', 'KRW': '₩' };
const NAMES: Record<string, string> = { 'CNY': '人民币', 'USD': '美元', 'EUR': '欧元', 'JPY': '日元', 'GBP': '英镑', 'KRW': '韩元' };
@Entry
@Component
struct index22 {
@State fromCurrency: string = 'CNY';
@State toCurrency: string = 'USD';
@State amount: string = '100';
@State result: string = '14.00';
convert(): void {
const val = parseFloat(this.amount);
if (isNaN(val) || val < 0) { this.result = '无效'; return; }
const rate = RATES[this.fromCurrency]?.[this.toCurrency];
if (rate === undefined) { this.result = '无汇率'; return; }
this.result = (val * rate).toFixed(2);
}
get rate(): string {
const r = RATES[this.fromCurrency]?.[this.toCurrency];
return (r !== undefined ? r : 0).toFixed(4);
}
build() {
Column() {
Row() {
Text('‹ 返回').fontSize(16).fontColor('#0077FF').onClick(() => router.back())
Text('💰 货币转换').fontSize(18).fontWeight(FontWeight.Bold)
Blank()
}.width('92%').padding({ top: 16, bottom: 8 })
Column() {
// 源货币输入面板
Column() {
Row() {
Text(SYMBOLS[this.fromCurrency]).fontSize(24).fontWeight(FontWeight.Bold).width(36)
TextInput({ text: this.amount })
.layoutWeight(1).height(48).backgroundColor('#fff').borderRadius(8)
.type(InputType.Number).padding({ left: 12 }).fontSize(20)
.onChange((v: string) => { this.amount = v; this.convert() })
}
Text(NAMES[this.fromCurrency]).fontSize(13).fontColor('#888')
}.width('92%').padding(12).backgroundColor('#fff').borderRadius(12)
// 互换按钮
Button() {
Text('⇅ 互换').fontSize(20).fontColor('#fff')
}.width(56).height(56).borderRadius(28).backgroundColor('#0077FF').margin(8)
.onClick(() => {
const t = this.fromCurrency;
this.fromCurrency = this.toCurrency;
this.toCurrency = t;
this.convert();
})
// 目标货币结果面板
Column() {
Row() {
Text(SYMBOLS[this.toCurrency]).fontSize(24).fontWeight(FontWeight.Bold).width(36)
Text(this.result)
.layoutWeight(1).height(48).fontSize(24).fontWeight(FontWeight.Bold)
.fontColor('#F44336').padding({ left: 12 })
}
Text(NAMES[this.toCurrency]).fontSize(13).fontColor('#888')
}.width('92%').padding(12).backgroundColor('#fff').borderRadius(12)
// 汇率展示
Text('汇率: 1 ' + this.fromCurrency + ' = ' + this.rate + ' ' + this.toCurrency)
.fontSize(13).fontColor('#888').margin({ top: 12 })
// 货币快速选择区
Flex({ wrap: FlexWrap.Wrap, justifyContent: FlexAlign.Center }) {
ForEach(CURRENCIES, (c: string) => {
Text(NAMES[c])
.fontSize(14).padding(8)
.backgroundColor(c === this.fromCurrency ? '#0077FF' : '#eee')
.fontColor(c === this.fromCurrency ? '#fff' : '#333')
.borderRadius(16).margin(4)
.onClick(() => { this.fromCurrency = c; this.convert() })
})
}.width('92%').margin({ top: 16 })
}
.layoutWeight(1).alignItems(HorizontalAlign.Center).padding({ top: 20 })
}
.width('100%').height('100%').backgroundColor('#f8f8f8')
}
}
3.2 汇率查找机制
const rate = RATES[this.fromCurrency]?.[this.toCurrency];
if (rate === undefined) { this.result = '无汇率'; return; }
this.result = (val * rate).toFixed(2);
这里使用了可选链操作符(?.),这是 ArkTS/TypeScript 提供的一种安全访问嵌套对象属性的语法:
RATES[this.fromCurrency]获取源货币的汇率字典?.[this.toCurrency]如果源货币字典存在,则继续查找目标货币汇率- 如果任一环节不存在,
rate的值为undefined
这种写法避免了多层 if 判断和可能的 Cannot read properties of undefined 运行时错误,是现代 ArkTS 开发中推荐的安全访问模式。
3.3 互换功能的实现
.onClick(() => {
const t = this.fromCurrency;
this.fromCurrency = this.toCurrency;
this.toCurrency = t;
this.convert();
})
互换操作使用了一个临时变量 t 来暂存 fromCurrency 的值,然后完成两个变量的交换。这种"三角交换"是编程中最基础的算法模式之一。交换后立即调用 convert() 重新计算结果,确保 UI 显示与最新状态同步。
3.4 Flex 弹性换行布局
Flex({ wrap: FlexWrap.Wrap, justifyContent: FlexAlign.Center }) {
ForEach(CURRENCIES, (c: string) => {
Text(NAMES[c])
.fontSize(14).padding(8)
.backgroundColor(c === this.fromCurrency ? '#0077FF' : '#eee')
.fontColor(c === this.fromCurrency ? '#fff' : '#333')
.borderRadius(16).margin(4)
.onClick(() => { this.fromCurrency = c; this.convert() })
})
}.width('92%').margin({ top: 16 })
Flex 组件是 HarmonyOS 中实现弹性布局的利器,支持主轴和交叉轴两个维度的灵活排列。本应用中使用了以下关键参数:
wrap: FlexWrap.Wrap:允许子项自动换行,当一行排不下时折到下一行justifyContent: FlexAlign.Center:主轴方向居中对齐
如果不使用 FlexWrap.Wrap,当货币标签较多时,它们会被压缩在一行内显示不全。启用换行后,标签会在多行中自然分布,适配不同屏幕宽度。
每个货币标签同样使用了状态驱动的样式切换:
.backgroundColor(c === this.fromCurrency ? '#0077FF' : '#eee')
.fontColor(c === this.fromCurrency ? '#fff' : '#333')
当前选中的源货币显示为蓝色背景白色文字,其他货币显示为灰色背景深色文字。点击任一货币标签,立即切换源货币并触发换算。
3.5 输入即计算的实时响应
TextInput({ text: this.amount })
// ...
.onChange((v: string) => { this.amount = v; this.convert() })
与"需要点击按钮才计算"的模式不同,本应用在 onChange 回调中直接调用 convert() 方法,实现了输入即计算的实时响应体验。用户每输入一个数字,换算结果就立即更新,这种交互模式在计算器类应用中尤为重要。
四、HarmonyOS 特性深度解析
4.1 Record 字典类型与类型安全
Record<K, V> 是 ArkTS 中用于定义字典(键值对)的类型工具。它的类型参数:
- K:键的类型(通常为
string或number) - V:值的类型
const RATES: Record<string, Record<string, number>> = { ... };
这表示 RATES 是一个字典,其键为字符串(货币代码),值为另一个字典(Record<string, number>——目标货币代码到汇率的映射)。
使用 Record 而非普通对象的好处:
- 类型约束:编译时检查键和值的类型是否正确
- 可读性:明确表达了"这是一个字典"的语义
- 安全性:访问不存在的键时返回
undefined而非抛出异常
4.2 TextInput 与 @State 的双向数据绑定
TextInput({ text: this.amount })
.onChange((v: string) => { this.amount = v; this.convert() })
在 ArkTS 中,TextInput 的 text 参数是单向绑定的——组件显示 @State 的值,但用户输入的变化需要通过 onChange 回调手动同步回 @State 变量。这种模式实现了外观上的双向绑定:
用户输入 → onChange → this.amount = v → UI 重新渲染 → TextInput 显示最新值
如果需要真正的双向绑定(即修改 @State 变量后,TextInput 自动更新其显示内容),则必须确保 text 参数始终引用最新的 @State 值。本应用中正是这样做的,因此当 convert() 方法没有修改 amount 时,TextInput 的内容不会意外变化。
4.3 布局权重的灵活运用
TextInput({ text: this.amount })
.layoutWeight(1) // 占据剩余空间
.height(48)
layoutWeight(1) 是 ArkTS 中实现弹性宽度的关键属性。在 Row 布局中,当货币符号(Text)固定宽度为36px,TextInput 通过 layoutWeight(1) 占据剩余的所有空间。这种设计确保:
- 货币符号始终可见且不压缩
- 输入框自适应屏幕宽度
- 在不同屏幕尺寸下都能完美适配
4.4 Button 的内容插槽
Button() {
Text('⇅ 互换').fontSize(20).fontColor('#fff')
}
HarmonyOS 的 Button 组件支持内容插槽(Content Slot),即可以在 Button 的尾随闭包中放入任意 UI 组件作为按钮的内容。这与传统移动开发中按钮只能显示文本的模式不同,ArkTS 的按钮可以是文字、图标、甚至复杂布局的组合。
五、UI/UX 设计模式
5.1 视觉层级设计
应用的视觉层级遵循"输入 → 操作 → 输出"的自然流程:
┌─────────────────────────────────┐
│ ① 源货币输入面板(白色卡片) │
│ 显示货币符号和名称 │
├─────────────────────────────────┤
│ ② 互换按钮(圆形蓝色按钮) │
├─────────────────────────────────┤
│ ③ 目标货币结果面板(白色卡片) │
│ 结果显示为红色大字体 │
├─────────────────────────────────┤
│ ④ 汇率信息(灰色小字) │
├─────────────────────────────────┤
│ ⑤ 货币选择区(Flex 标签) │
└─────────────────────────────────┘
这种从上到下的视觉引导,让用户自然完成"输入→查看结果"的任务流程。
5.2 输入与结果面板对比
源货币面板和目标货币面板采用了对称设计:
| 元素 | 源货币面板 | 目标货币面板 |
|---|---|---|
| 背景 | 白色卡片 | 白色卡片 |
| 符号 | 24px 粗体 | 24px 粗体 |
| 内容 | TextInput 可编辑 | Text 只读显示 |
| 结果颜色 | 默认黑色 | 红色强调 |
| 货币名称 | 灰色小字 | 灰色小字 |
两个面板的高度、内边距、圆角都保持一致,形成视觉上的对仗感。唯一的区别在于——源货币面板是可编辑的输入框,目标货币面板是只读的结果展示——这种"同中有异"的设计清晰传达了各自的功能角色。
5.3 互换按钮的圆形设计
Button() {
Text('⇅ 互换').fontSize(20).fontColor('#fff')
}.width(56).height(56).borderRadius(28).backgroundColor('#0077FF').margin(8)
互换按钮被设计为圆形(borderRadius(28) 等于宽度56的一半),这是一种符合 Material Design 规范的 FAB(Floating Action Button,浮动操作按钮)风格。圆形按钮在两个矩形面板之间形成视觉节奏的变化,同时"⇅"符号直观传达了"上下互换"的操作含义。
六、最佳实践与经验总结
6.1 汇率数据管理
本应用使用硬编码的模拟汇率数据,适用于演示和学习场景。在生产环境中,建议:
- 使用远程 API:通过
http模块从第三方汇率服务获取实时汇率 - 本地缓存:将获取的汇率缓存到
Preferences中,减少网络请求 - 定时更新:使用
setInterval定期刷新汇率数据 - 离线容错:当网络不可用时,使用缓存或最后已知的汇率
6.2 货币代码标准化
使用 ISO 4217 标准的三字母货币代码(CNY、USD、EUR 等)作为货币标识,这是国际金融领域的通行做法。好处包括:
- 通用性:全球通用的标准代码
- 可扩展性:易于添加新货币
- 兼容性:与外部 API 的数据格式兼容
6.3 输入防抖考量
在实时换算场景中,如果未来接入网络 API 获取实时汇率,频繁的 onChange 触发可能导致大量网络请求。此时应考虑输入防抖(Debounce)机制:
// 防抖示例(非本应用代码)
let debounceTimer: number = -1;
onChange((v: string) => {
this.amount = v;
clearTimeout(debounceTimer);
debounceTimer = setTimeout(() => this.convert(), 300);
});
不过在本应用中,换算是纯本地计算,性能开销极小,因此无需防抖。
6.4 可选链的安全访问
const rate = RATES[this.fromCurrency]?.[this.toCurrency];
这种写法等价于:
const fromRates = RATES[this.fromCurrency];
const rate = fromRates !== undefined ? fromRates[this.toCurrency] : undefined;
可选链操作符让代码更简洁、更安全,是 ArkTS 开发中推荐使用的现代语法特性。
6.5 国际化考虑
本应用已经包含了中文货币名称。如果要支持多语言,可以将 NAMES 设计为按语言分组的嵌套字典:
const NAMES: Record<string, Record<string, string>> = {
'zh': { 'CNY': '人民币', ... },
'en': { 'CNY': 'Chinese Yuan', ... },
};
七、总结
货币转换应用在约90行代码中,完美展示了 ArkTS 声明式 UI 开发的多个核心概念:
- Record 字典数据结构:O(1) 时间复杂度的高效数据查询
- Flex 弹性换行布局:自适应屏幕宽度的标签排列
- @State 状态驱动:自动触发的 UI 渲染更新
- 实时计算反馈:输入即计算的流畅交互体验
- 可选链安全访问:优雅的嵌套数据访问方式
- 状态驱动的样式切换:视觉反馈与功能状态绑定
这个应用不仅是一个实用的汇率换算工具,更是一个数据驱动 UI 开发的典范案例。它清晰地展示了如何将数据结构、业务逻辑和 UI 表示层有机地组织在一起,构建出既高效又美观的应用程序。对于希望深入学习 ArkTS 数据管理和布局技术的开发者来说,货币转换应用提供了一个绝佳的参考范本。
更多推荐


所有评论(0)