【maaath】Flutter for OpenHarmony 手表配饰应用实战开发

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 开发鸿蒙应用的核心流程和关键技巧。通过本项目的实践,我们可以看到:

  1. 跨平台开发效率提升:使用 Flutter for OpenHarmony 可以实现一次开发、多端部署,大大提升了开发效率。

  2. ArkTS 语法特性:开发过程中需要注意 ArkTS 相比标准 TypeScript 的语法限制,如类型推断、对象字面量等。

  3. ArkUI 组件使用:合理使用 ArkUI 提供的丰富组件,如 Stack、Grid、List 等,可以构建出精美的用户界面。

  4. 动态效果实现:通过状态变量和动画属性,可以实现流畅的交互动画效果,提升用户体验。

希望本文能够为正在学习或准备使用 Flutter for OpenHarmony 进行开发的读者提供有价值的参考,共同推动鸿蒙跨平台生态的发展。


参考资料

  • Flutter for OpenHarmony 官方文档
  • ArkUI 组件开发指南
  • OpenHarmony 应用开发规范

相关推荐
maaath2 小时前
【maaath】Flutter for OpenHarmony 跨平台计算器应用开发实践
flutter·华为·harmonyos
以太浮标3 小时前
华为eNSP模拟器综合实验之- MGRE多点GRE隧道详解
运维·网络·网络协议·网络安全·华为·信息与通信
前端不太难7 小时前
鸿蒙PC和App:都在走向 System
华为·状态模式·harmonyos
maaath7 小时前
【maaath】Flutter for OpenHarmony 闹钟时钟应用开发实战
flutter·华为·harmonyos
maaath8 小时前
【maaath】Flutter for OpenHarmony 短信管理应用实战
flutter·华为·harmonyos
程序猿追8 小时前
从零打造一个“跳一跳”:在HarmonyOS模拟器上用Canvas复刻经典
华为·harmonyos
@不误正业8 小时前
第13章-开源鸿蒙是否适合做端侧AI操作系统
人工智能·开源·harmonyos
UnicornDev8 小时前
【HarmonyOS 6】底部悬浮导航的迷你栏适配(API23)
华为·harmonyos·arkts·鸿蒙
@不误正业8 小时前
OpenHarmony-A2A协议实战-多智能体跨应用协同架构与实现
人工智能·架构·harmonyos·开源鸿蒙