Flutter for OpenHarmony 手表配饰应用实战开发
作者:maaath
社区引导
欢迎加入开源鸿蒙跨平台社区: https://openharmonycrossplatform.csdn.net
前言
随着鸿蒙生态的快速发展,Flutter for OpenHarmony 作为跨平台开发框架,为开发者提供了高效构建鸿蒙应用的能力。本文将通过一个手表配饰商品展示应用的实际案例,详细讲解如何使用 Flutter for OpenHarmony 开发具有良好用户体验的鸿蒙应用,并展示其在真机上的运行效果。
一、项目概述
1.1 项目背景
手表配饰应用是一个面向消费者的商品展示类应用,主要功能包括:
- 手表商品列表展示与筛选
- 手表详情查看(支持表带搭配、表盘切换)
- 表盘管理功能
- 用户收藏与足迹
- 设置页面
1.2 技术选型
- 框架:Flutter for OpenHarmony 3.12+
- 状态管理:ArkTS 原生状态管理
- 路由:鸿蒙原生路由机制
- UI 组件:基于 ArkUI 组件库
二、项目结构
entry/
├── src/main/ets/
│ ├── entryability/
│ │ └── EntryAbility.ets # 应用入口
│ ├── pages/
│ │ ├── Index.ets # 首页
│ │ └── MainPage.ets # 主页面
│ ├── view/
│ │ ├── components/ # 可复用组件
│ │ │ ├── WatchCard.ets # 手表卡片组件
│ │ │ └── WatchFaceCard.ets # 表盘卡片组件
│ │ ├── model/ # 数据模型
│ │ │ └── WatchModel.ets # 手表数据模型
│ │ └── pages/ # 页面组件
│ │ ├── WatchDiscoverPage.ets # 发现页面
│ │ ├── WatchDetailPage.ets # 详情页面
│ │ ├── WatchFacePage.ets # 表盘页面
│ │ ├── WatchMinePage.ets # 我的页面
│ │ └── WatchSettingsPage.ets # 设置页面
│ └── utils/
│ └── Logger.ets # 日志工具
└── src/main/resources/
└── base/profile/
└── main_pages.json # 页面路由配置
三、核心功能实现
3.1 数据模型定义
首先定义手表和表盘的数据模型:
dart
// WatchModel.ets
export interface Watch {
id: string;
name: string;
brand: string;
price: number;
originalPrice: number;
imageUrl: string;
thumbnailUrl: string;
description: string;
color: string;
material: string;
caseSize: string;
waterResistance: string;
releaseDate: string;
status: string;
rating: number;
salesCount: number;
isFavorite: boolean;
images: string[];
straps: WatchStrap[];
}
export interface WatchStrap {
id: string;
name: string;
color: string;
material: string;
price: number;
imageUrl: string;
}
export interface WatchFace {
id: string;
name: string;
previewUrl: string;
style: string;
isPremium: boolean;
isInstalled: boolean;
}
3.2 手表卡片组件
手表卡片是商品列表的核心展示组件,需要展示商品图片、价格、评分等信息:
dart
@Component
export struct WatchCard {
@Prop watch: Watch;
@Prop index: number = 0;
build() {
Column() {
Stack({ alignContent: Alignment.TopEnd }) {
Column() {
Image(this.watch.thumbnailUrl)
.width('100%')
.height(160)
.objectFit(ImageFit.Contain)
.backgroundColor('#F5F5F5')
.borderRadius({ topLeft: 12, topRight: 12 })
.alt($r('app.media.icon'))
}
.width('100%')
Row() {
Image(this.watch.isFavorite ? $r('app.media.icon') : $r('app.media.icon'))
.width(24)
.height(24)
.fillColor(this.watch.isFavorite ? '#FF4081' : '#999999')
}
.width(36)
.height(36)
.backgroundColor('rgba(255,255,255,0.9)')
.borderRadius(18)
.justifyContent(FlexAlign.Center)
.margin({ right: 8, top: 8 })
}
.width('100%')
Column() {
Text(this.watch.brand)
.fontSize(11)
.fontColor('#999999')
.maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis });
Text(this.watch.name)
.fontSize(14)
.fontColor('#333333')
.fontWeight(FontWeight.Medium)
.maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis })
.margin({ top: 4 });
Row() {
Text('¥' + this.watch.price)
.fontSize(16)
.fontColor('#FF4081')
.fontWeight(FontWeight.Bold);
Text('¥' + this.watch.originalPrice)
.fontSize(11)
.fontColor('#999999')
.decoration({ type: TextDecorationType.LineThrough })
.margin({ left: 6 });
}
.width('100%')
.margin({ top: 6 });
Row() {
Row() {
ForEach([1, 2, 3, 4, 5], (star: number) => {
Image($r('app.media.icon'))
.width(10)
.height(10)
.fillColor(star <= Math.round(this.watch.rating) ? '#FFD700' : '#DDDDDD');
});
Text(this.watch.rating.toFixed(1))
.fontSize(10)
.fontColor('#666666')
.margin({ left: 4 });
}
Text('销量 ' + this.watch.salesCount)
.fontSize(10)
.fontColor('#999999')
.margin({ left: 'auto' });
}
.width('100%')
.margin({ top: 4 });
}
.width('100%')
.padding(10)
.alignItems(HorizontalAlign.Start)
}
.width('48%')
.backgroundColor('#FFFFFF')
.borderRadius(12)
.shadow({
radius: 8,
color: 'rgba(0,0,0,0.08)',
offsetX: 0,
offsetY: 2
})
.onClick(() => {
router.pushUrl({
url: 'view/pages/WatchDetailPage',
params: { watchId: this.watch.id }
});
})
}
}
3.3 表盘预览组件
表盘是手表应用的重要特色功能,我们实现了动态时钟效果的表盘预览:
dart
@Component
export struct WatchFacePreview {
@Prop faceName: string;
@Prop faceStyle: string = 'classic';
@State currentTime: Date = new Date();
@State secondAngle: number = 0;
aboutToAppear(): void {
this.updateTime();
setInterval(() => {
this.updateTime();
}, 1000);
}
private updateTime(): void {
this.currentTime = new Date();
this.secondAngle = (this.currentTime.getSeconds() / 60) * 360;
}
build() {
Stack() {
Circle()
.width(160)
.height(160)
.fill(this.getBackgroundColor());
// 刻度线
ForEach([0, 30, 60, 90, 120, 150, 180, 210, 240, 270, 300, 330], (angle: number) => {
Line()
.startPoint([80, 0])
.endPoint([80, angle % 90 === 0 ? 10 : 6])
.stroke(this.getTickColor())
.strokeWidth(2)
.rotate({
angle: angle,
centerX: 0,
centerY: 0
});
});
// 时针
Line()
.startPoint([0, 30])
.endPoint([0, -15])
.stroke(this.getHandColor())
.strokeWidth(3)
.rotate({
angle: ((this.currentTime.getHours() % 12) * 30 + this.currentTime.getMinutes() * 0.5),
centerX: 0,
centerY: 0
});
// 分针
Line()
.startPoint([0, 40])
.endPoint([0, -25])
.stroke(this.getHandColor())
.strokeWidth(2)
.rotate({
angle: (this.currentTime.getMinutes() * 6),
centerX: 0,
centerY: 0
});
// 秒针
Line()
.startPoint([0, 45])
.endPoint([0, -35])
.stroke('#FF4081')
.strokeWidth(1.5)
.rotate({
angle: this.secondAngle,
centerX: 0,
centerY: 0
});
// 中心点
Circle()
.width(6)
.height(6)
.fill(this.getHandColor());
Text(this.faceName)
.fontSize(10)
.fontColor(this.faceStyle === 'modern' ? 'rgba(255,255,255,0.7)' : 'rgba(0,0,0,0.4)')
.position({ y: 50 });
}
.width(160)
.height(160);
}
private getBackgroundColor(): string {
if (this.faceStyle === 'classic') return '#FDF5E6';
if (this.faceStyle === 'modern') return '#1A1A2E';
if (this.faceStyle === 'sport') return '#2C3E50';
return '#FAFAFA';
}
private getTickColor(): string {
if (this.faceStyle === 'modern') return 'rgba(255,255,255,0.5)';
return 'rgba(0,0,0,0.4)';
}
private getHandColor(): string {
if (this.faceStyle === 'modern') return '#FFFFFF';
return '#333333';
}
}
3.4 手表详情页
详情页展示手表的完整信息,包括表带选择和表盘切换功能:
dart
@Component
export struct WatchDetailPage {
@State watch: Watch | null = null;
@State selectedStrap: WatchStrap | null = null;
@State selectedFace: WatchFace | null = null;
@State strapTransitioning: boolean = false;
@State faceTransitioning: boolean = false;
aboutToAppear(): void {
this.loadWatchDetail();
}
private delay(ms: number): Promise<void> {
return new Promise<void>(resolve => setTimeout(resolve, ms));
}
async loadWatchDetail(): Promise<void> {
const params = router.getParams() as Record<string, string>;
const watchId = params?.watchId || 'w1';
this.isLoading = true;
try {
await this.delay(500);
this.watch = this.getMockWatch(watchId);
if (this.watch && this.watch.straps.length > 0) {
this.selectedStrap = this.watch.straps[0];
}
this.selectedFace = this.availableFaces[0];
} finally {
this.isLoading = false;
}
}
selectStrap(strap: WatchStrap): void {
this.strapTransitioning = true;
setTimeout(() => {
this.selectedStrap = strap;
this.strapTransitioning = false;
promptAction.showToast({ message: `已选择「${strap.name}」` });
}, 300);
}
build() {
Column() {
this.buildHeader();
if (this.isLoading) {
this.buildLoadingState();
} else if (this.watch) {
this.buildContent();
}
}
.width('100%')
.height('100%')
.backgroundColor('#F5F5F5');
}
}
3.5 主页面与底部导航
主页面使用 Tab 底部导航实现多页面切换:
dart
@Entry
@Component
struct MainPage {
@State currentIndex: number = 0;
@Builder
TabBuilder(title: string, icon: Resource, selectedIcon: Resource, index: number) {
Column() {
Image(this.currentIndex === index ? selectedIcon : icon)
.width(24)
.height(24)
.fillColor(this.currentIndex === index ? '#FF4081' : '#666666');
Text(title)
.fontSize(10)
.fontColor(this.currentIndex === index ? '#FF4081' : '#666666')
.margin({ top: 4 });
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center);
}
build() {
Tabs({ barPosition: BarPosition.End }) {
TabContent() {
WatchDiscoverPage()
}
.tabBar(this.TabBuilder('发现', $r('app.media.icon'), $r('app.media.icon'), 0))
TabContent() {
WatchFacePage()
}
.tabBar(this.TabBuilder('表盘', $r('app.media.icon'), $r('app.media.icon'), 1))
TabContent() {
WatchMinePage()
}
.tabBar(this.TabBuilder('我的', $r('app.media.icon'), $r('app.media.icon'), 2))
}
.barHeight(60)
.onChange((index: number) => {
this.currentIndex = index;
});
}
}
四、ArkTS 语法注意事项
在使用 Flutter for OpenHarmony 开发时,需要注意以下几点 ArkTS 语法规范:
4.1 类型推断限制
ArkTS 对泛型函数调用的类型推断有限制,建议显式声明返回类型:
dart
// 推荐写法
async getWatchList(page: number): Promise<Watch[]> {
const delay = (ms: number): Promise<void> => {
return new Promise<void>(resolve => setTimeout(resolve, ms));
};
await delay(500);
return this.createMockWatches(page);
}
// 不推荐:类型推断可能失败
async getWatchList(page: number) {
await new Promise(resolve => setTimeout(resolve, 500));
return this.createMockWatches(page);
}
4.2 对象字面量类型
对象字面量必须对应显式声明的接口或类:
dart
// 推荐:定义接口后使用
interface StyleItem {
key: string;
name: string;
}
private styles: StyleItem[] = [
{ key: '', name: '全部' },
{ key: 'classic', name: '经典' }
];
// 不推荐:匿名对象字面量
private styles: Array<{ key: string, name: string }> = [...];
4.3 组件属性
注意 ArkUI 组件不支持的属性:
- Line 组件不支持
lineCap属性 - Row/Column 组件不支持
scrollable属性,需使用 Scroll 组件 - List 组件不支持
cachedCount参数
五、运行效果展示
5.1 发现页面
发现页面展示了手表的瀑布流布局,支持品牌筛选功能。用户可以通过顶部标签快速筛选不同品牌的手表商品,每个商品卡片展示了商品图片、价格、评分和销量信息。
5.2 详情页面
详情页面展示了手表的完整信息,包括高清图片、价格信息、商品参数等。特别提供了表带搭配功能,用户可以选择不同的表带款式,并实时预览效果。
5.3 表盘页面
表盘页面提供了丰富的表盘选择,支持多种风格切换。预览区域实时显示当前选中表盘的模拟效果,包含动态时钟指针。
5.4 我的页面
个人中心页面展示用户收藏、浏览足迹等信息,并提供了丰富的功能入口,方便用户快速访问常用功能。
六、截图运行验证
为验证代码在鸿蒙设备上的可运行性,以下展示应用在鸿蒙设备上的实际运行截图:
6.1 设置页面运行截图
设置页面正常展示设置信息、功能菜单。用户可自由设置相关应用和权限

6.2 表盘页面运行截图
表盘页面的当前预览表盘正常显示动态时间,已安装表盘列表正常展示。表盘网格列表加载正常,点击切换表盘功能正常可用。

6.3 我的页面运行截图
我的页面正常展示用户信息、统计数据和功能菜单。收藏列表和浏览足迹可正常加载,点击商品可正常跳转至详情页面。

运行验证结论:应用在鸿蒙设备上可正常安装、启动和运行,所有核心功能均可正常使用,未发现重大逻辑错误。
七、代码仓库
本项目的完整代码已托管至 AtomGit 平台:
仓库地址:https://atomgit.com/maaath/watch-app-flutter
仓库中包含了完整的项目源代码、详细的 README 文档以及开发过程中的注意事项说明。
八、总结
本文通过手表配饰应用的实际开发案例,展示了 Flutter for OpenHarmony 开发鸿蒙应用的核心流程和关键技巧。通过本项目的实践,我们可以看到:
-
跨平台开发效率提升:使用 Flutter for OpenHarmony 可以实现一次开发、多端部署,大大提升了开发效率。
-
ArkTS 语法特性:开发过程中需要注意 ArkTS 相比标准 TypeScript 的语法限制,如类型推断、对象字面量等。
-
ArkUI 组件使用:合理使用 ArkUI 提供的丰富组件,如 Stack、Grid、List 等,可以构建出精美的用户界面。
-
动态效果实现:通过状态变量和动画属性,可以实现流畅的交互动画效果,提升用户体验。
希望本文能够为正在学习或准备使用 Flutter for OpenHarmony 进行开发的读者提供有价值的参考,共同推动鸿蒙跨平台生态的发展。
参考资料
- Flutter for OpenHarmony 官方文档
- ArkUI 组件开发指南
- OpenHarmony 应用开发规范