【鸿蒙原生应用实战】第一篇:项目搭建与首页开发——从零构建户外助手App

前言

鸿蒙生态正在快速发展,ArkTS + Stage 模型已经成为鸿蒙原生应用开发的标准技术栈。本系列文章将以一个完整的 「户外助手」 App 为案例,从项目初始化到页面开发、交互实现、优化发布,连续五篇带你走完一个真实鸿蒙项目的开发全流程。

本 App 的功能包括:

  • 首页概览:行程统计、天气信息、快捷入口
  • 装备库管理:分类筛选、重量统计、季节推荐
  • 装备详情:参数展示、状况评估、保养指南
  • 打包清单:按活动类型生成清单、勾选打包、重量预估
  • 活动记录:按年份筛选、成就系统、季节分布

本文是系列第一篇,聚焦项目创建、工程结构理解和首页开发。


一、项目创建与工程配置

1.1 使用 DevEco Studio 创建项目

打开 DevEco Studio,选择 File → New → Create Project,选择 Empty Ability 模板。

关键配置:

  • Project Name: MyApplication
  • Bundle Name: com.example.myapplication
  • Compatible SDK: API 23 (HarmonyOS 6.1.0)
  • Target SDK: API 24 (HarmonyOS 6.1.1)
  • Model: Stage
  • Language: ArkTS

1.2 工程目录结构

项目创建完成后,我们需要理解鸿蒙 Stage 模型的目录结构:

MyApplication/
├── AppScope/                     # 全局应用配置
│   ├── app.json5                 # 应用级配置(bundleName、版本号等)
│   └── resources/base/element/   # 全局资源
├── entry/                        # 应用模块
│   ├── src/main/
│   │   ├── ets/
│   │   │   ├── entryability/     # Ability 生命周期
│   │   │   └── pages/            # 页面文件
│   │   ├── module.json5          # 模块配置
│   │   └── resources/            # 资源文件
│   ├── build-profile.json5       # 模块构建配置
│   └── oh-package.json5         # 依赖管理
├── build-profile.json5           # 项目级构建配置
└── hvigor/                       # 构建工具配置

1.3 Stage 模型的核心概念

Stage 模型是鸿蒙从 API 9 开始主推的 Ability 框架,核心思想:

┌─────────────────────────────────────┐
│            UIAbility                │  ← 应用入口,管理生命周期
├─────────────────────────────────────┤
│          WindowStage                │  ← 窗口管理
├─────────────────────────────────────┤
│         pages/Index.ets             │  ← 页面(一个 Ability 可加载多个页面)
├─────────────────────────────────────┤
│          @Module                    │  ← 模块化组织
└─────────────────────────────────────┘

entryability/EntryAbility.ets 中,我们看到标准的生命周期代码:

// EntryAbility.ets
import { AbilityConstant, ConfigurationConstant, UIAbility, Want } from '@kit.AbilityKit';
import { hilog } from '@kit.PerformanceAnalysisKit';
import { window } from '@kit.ArkUI';

export default class EntryAbility extends UIAbility {
  onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
    try {
      this.context.getApplicationContext().setColorMode(
        ConfigurationConstant.ColorMode.COLOR_MODE_NOT_SET
      );
    } catch (err) {
      hilog.error(DOMAIN, 'testTag', 'Failed to set colorMode. Cause: %{public}s', JSON.stringify(err));
    }
  }

  onWindowStageCreate(windowStage: window.WindowStage): void {
    windowStage.loadContent('pages/Index', (err) => {
      if (err.code) {
        hilog.error(DOMAIN, 'testTag', 'Failed to load the content. Cause: %{public}s', JSON.stringify(err));
        return;
      }
      hilog.info(DOMAIN, 'testTag', 'Succeeded in loading the content.');
    });
  }
  // ... 其他生命周期方法
}

关键点windowStage.loadContent('pages/Index', callback) 就是加载首页页面的入口。


二、路由注册

2.1 main_pages.json 配置

页面路由在 resources/base/profile/main_pages.json 中注册,相当于一个路由表:

{
  "src": [
    "pages/Index",
    "pages/GearPage",
    "pages/GearDetailPage",
    "pages/PackPage",
    "pages/ActivityRecordPage"
  ]
}

注意

  • 路径相对于 ets/ 目录,不需要写 ets/ 前缀
  • 不需要写文件扩展名 .ets
  • 每个页面都需要在这里注册才能被路由跳转

2.2 页面跳转方式

鸿蒙提供了 @ohos.router 模块实现页面跳转:

import router from '@ohos.router';

// 不带参数跳转
router.pushUrl({ url: 'pages/GearPage' });

// 带参数跳转
router.pushUrl({
  url: 'pages/GearDetailPage',
  params: { gearId: 1 }
});

// 返回上一页
router.back();

三、首页页面开发

3.1 数据模型定义

首先定义 Activity 接口,描述一次户外活动的数据结构:

// Index.ets
interface Activity {
  id: number;
  title: string;       // 活动标题
  type: string;        // 活动类型:徒步/骑行/露营/登山
  date: string;        // 日期
  location: string;    // 地点
  duration: string;    // 时长
  participants: number; // 参与人数
  icon: string;        // 表情图标
  status: string;      // 状态:planned(计划中) / completed(已完成)
}

3.2 状态变量与数据初始化

使用 @State 装饰器声明响应式状态:

@Entry
@Component
struct Index {
  @State activities: Activity[] = [];
  @State totalTrips: number = 0;
  @State totalDays: number = 0;
  @State gearCount: number = 0;

  aboutToAppear(): void {
    this.loadData();
  }

  loadData(): void {
    this.totalTrips = 12;
    this.totalDays = 36;
    this.gearCount = 28;

    this.activities = [
      { id: 1, title: '黄山徒步之旅', type: '徒步', date: '2025-03-15',
        location: '安徽黄山', duration: '3天2夜', participants: 4,
        icon: '🏔️', status: 'planned' },
      { id: 2, title: '西湖骑行', type: '骑行', date: '2025-02-20',
        location: '浙江杭州', duration: '1天', participants: 2,
        icon: '🚴', status: 'completed' },
      // ... 更多活动数据
    ];
  }
}

aboutToAppear 是 ArkTS 的生命周期方法,类似于 Flutter 的 initState 或 Android 的 onCreateView,在组件初始化时被调用。

3.3 构建 Header 头部

第一个 Builder 是页面的头部区域:

@Builder buildHeader() {
  Column() {
    Row() {
      Column() {
        Text('🏕️ 户外助手')
          .fontSize(22).fontWeight(FontWeight.Bold).fontColor('#1A1A2E')
        Text('探索自然,记录精彩')
          .fontSize(13).fontColor('#999999').margin({ top: 4 })
      }
      .alignItems(HorizontalAlign.Start)

      Blank()  // 弹性空白,将头像推到右侧
      Stack() {
        Column()
          .width(40).height(40).borderRadius(20).backgroundColor('#E8F5E9')
          .justifyContent(FlexAlign.Center).alignItems(HorizontalAlign.Center)
        Text('🏔️').fontSize(18)
      }
    }
    .width('100%').padding({ left: 16, right: 16, top: 12 })
  }
  .width('100%').backgroundColor('#FFFFFF').padding({ bottom: 8 })
}

ArkTS 布局技巧

  • Blank() 组件会自动填充剩余空间,实现左右对齐
  • Stack + Column 实现圆形背景+文字头像
  • .borderRadius(20) 配合 width=40, height=40 实现正圆形

3.4 统计行(Stats Row)

展示总行程、累计天数、装备总数、已完成数量四个核心指标:

@Builder buildStats() {
  Row() {
    Column() {
      Text(this.totalTrips.toString())
        .fontSize(22).fontWeight(FontWeight.Bold).fontColor('#FF6B35')
      Text('总行程').fontSize(11).fontColor('#999999').margin({ top: 2 })
    }
    .layoutWeight(1).alignItems(HorizontalAlign.Center)

    // 类似结构:累计天数(蓝色)、装备总数(绿色)、已完成(紫色)
    // ...
  }
  .width('100%').padding(14)
  .backgroundColor('#FFFFFF').borderRadius(10)
  .margin({ top: 8, left: 16, right: 16 })
}

layoutWeight(1) 是 ArkTS 中实现等分布局的利器,类似于 Flexbox 的 flex: 1。四个 Column 各占 1/4 宽度。

3.5 快捷操作区

四个图标按钮实现页面跳转:

@Builder buildQuickActions() {
  Row() {
    Column() {
      Text('🎒').fontSize(24)
      Text('装备库').fontSize(11).fontColor('#666666').margin({ top: 4 })
    }
    .layoutWeight(1).alignItems(HorizontalAlign.Center)
    .onClick(() => { router.pushUrl({ url: 'pages/GearPage' }); })

    Column() {
      Text('📝').fontSize(24)
      Text('打包清单').fontSize(11).fontColor('#666666').margin({ top: 4 })
    }
    .layoutWeight(1).alignItems(HorizontalAlign.Center)
    .onClick(() => { router.pushUrl({ url: 'pages/PackPage' }); })

    Column() {
      Text('➕').fontSize(24)
      Text('新活动').fontSize(11).fontColor('#666666').margin({ top: 4 })
    }
    .layoutWeight(1).alignItems(HorizontalAlign.Center)

    Column() {
      Text('📊').fontSize(24)
      Text('统计').fontSize(11).fontColor('#666666').margin({ top: 4 })
    }
    .layoutWeight(1).alignItems(HorizontalAlign.Center)
    .onClick(() => { router.pushUrl({ url: 'pages/GearPage' }); })
  }
  .width('100%').padding(14)
  .backgroundColor('#FFFFFF').borderRadius(10)
  .margin({ top: 8, left: 16, right: 16 })
}

注意:我们在 Column 上直接挂 onClick,而不是在 Text 上,这样可以扩大点击热区,提升用户体验。

3.6 天气小组件

模拟展示今日天气信息,包含 Emoji 图标和详情入口:

@Builder buildWeatherWidget() {
  Row() {
    Column() { Text('🌤️').fontSize(32) }
    Column() {
      Text('今日天气').fontSize(12).fontColor('#999999')
      Text('晴 · 8°C / -2°C')
        .fontSize(15).fontWeight(FontWeight.Bold).fontColor('#333333').margin({ top: 2 })
      Text('适宜户外活动,西北风3-4级').fontSize(11).fontColor('#BBBBBB').margin({ top: 2 })
    }
    .alignItems(HorizontalAlign.Start).margin({ left: 10 }).layoutWeight(1)
    Column() { Text('查看详情 >').fontSize(11).fontColor('#FF6B35') }
      .alignItems(HorizontalAlign.End)
  }
  .width('100%').padding(14)
  .backgroundColor('#FFFFFF').borderRadius(10)
  .margin({ top: 8, left: 16, right: 16 })
}

3.7 即将出发与过往行程

两个 section 展示活动列表,使用 ForEach 循环渲染:

@Builder buildUpcomingSection() {
  Column() {
    Row() {
      Text('📅 即将出发').fontSize(15).fontWeight(FontWeight.Bold).fontColor('#1A1A2E')
      Blank()
      Text('查看全部 >').fontSize(12).fontColor('#FF6B35')
    }
    .width('100%').padding({ left: 16, right: 16, top: 16 })

    ForEach(this.activities.filter((a: Activity) => a.status === 'planned'),
      (act: Activity) => {
        Row() {
          // 图标容器
          Stack() {
            Column().width(50).height(50).borderRadius(10).backgroundColor('#FFF0E8')
              .justifyContent(FlexAlign.Center).alignItems(HorizontalAlign.Center)
            Text(act.icon).fontSize(24)
          }
          // 文字信息
          Column() {
            Text(act.title).fontSize(14).fontWeight(FontWeight.Medium).fontColor('#333333')
            Text(`${act.location} · ${act.date} · ${act.duration}`)
              .fontSize(11).fontColor('#999999').margin({ top: 2 })
            Text(`👥 ${act.participants}人同行`)
              .fontSize(11).fontColor('#BBBBBB').margin({ top: 2 })
          }
          .alignItems(HorizontalAlign.Start).margin({ left: 10 }).layoutWeight(1)
        }
        .width('100%').padding(12)
        .backgroundColor('#FFFFFF').borderRadius(10)
        .margin({ top: 8, left: 16, right: 16 })
        .onClick(() => {
          router.pushUrl({ url: 'pages/PackPage', params: { activityId: act.id } });
        })
      }, (act: Activity) => act.id.toString())
  }
  .width('100%')
}

ForEach 的三个参数

  1. 数据源数组
  2. 构建函数 (item, index?) => void
  3. 键值生成器 (item) => string——用于列表 diff 优化,必须保证唯一

3.8 安全须知

底部的安全提醒模块:

@Builder buildSafetyTips() {
  Column() {
    Text('⚠️ 安全须知')
      .fontSize(14).fontWeight(FontWeight.Bold).fontColor('#E74C3C').width('100%')
    // 4条安全建议,每条用 Row 展示
    Row() {
      Text('•').fontSize(12).fontColor('#E74C3C')
      Text(' 出行前告知家人行程和预计返回时间')
        .fontSize(12).fontColor('#666666').margin({ left: 4 })
    }.width('100%').margin({ top: 4 })
    // ...更多
  }
  .width('100%').padding(16)
  .backgroundColor('#FFF5F5').borderRadius(10)
  .margin({ top: 8, left: 16, right: 16 })
  .alignItems(HorizontalAlign.Start)
}

3.9 组装 build 方法

最后一个步骤,把所有 Builder 组合起来:

build(): void {
  Column() {
    this.buildHeader()
    Scroll() {
      Column() {
        this.buildStats()
        this.buildWeatherWidget()
        this.buildQuickActions()
        this.buildSeasonRecommendation()
        this.buildUpcomingSection()
        this.buildHistorySection()
        this.buildSafetyTips()
      }
      .width('100%').padding({ bottom: 30 })
    }
    .scrollable(ScrollDirection.Vertical)
    .layoutWeight(1).width('100%')
  }
  .width('100%').height('100%').backgroundColor('#F5F5F5')
}

整个页面的布局层次

Column (全屏, 灰色背景)
  ├── buildHeader()              ← 头部:标题+头像
  └── Scroll                     ← 可滚动内容区
      └── Column
          ├── buildStats()       ← 四格统计
          ├── buildWeatherWidget() ← 天气
          ├── buildQuickActions()  ← 快捷入口
          ├── buildSeasonRecommendation() ← 季节推荐
          ├── buildUpcomingSection()      ← 即将出发列表
          ├── buildHistorySection()       ← 过往行程列表
          └── buildSafetyTips()          ← 安全须知

四、理解 @Builder 装饰器

这是 ArkTS 中非常核心的一个概念。@Builder 用于定义可复用的 UI 片段:

// 在 struct 内定义,可以访问 @State 变量
@Builder buildHeader() {
  // UI 描述
}

// 在 build() 方法中调用
this.buildHeader()

@Builder vs 普通函数

特性 @Builder 普通函数
支持组件声明式语法
可访问 @State
可带参数
自定义组件复用 推荐 不推荐

Builder 链式调用:每个 UI 组件都采用链式写法设置属性,如 .fontSize(22).fontWeight(FontWeight.Bold).fontColor('#1A1A2E'),这是 ArkTS 声明式 UI 的标准写法。


五、资源文件的正确使用

5.1 颜色资源 (color.json)

{
  "color": [
    { "name": "primary_color", "value": "#FF6B35" },
    { "name": "background_color", "value": "#F5F5F5" },
    { "name": "header_bg", "value": "#1A1A2E" }
  ]
}

引用方式:$color('primary_color')$r('app.color.primary_color')

5.2 尺寸资源 (float.json)

{
  "float": [
    { "name": "title_font_size", "value": "22fp" },
    { "name": "subtitle_font_size", "value": "16fp" },
    { "name": "card_radius", "value": "12vp" }
  ]
}

引用方式:$r('app.float.title_font_size')

fp 和 vp 的区别

  • vp (virtual pixel):虚拟像素,与密度无关
  • fp (font pixel):字体像素,会跟随系统字体大小设置缩放

六、开发小技巧

6.1 使用多个 @Builder 拆分复杂页面

把一个复杂的 build() 拆分成多个 @Builder,每个只负责一个区块:

  • 有利于代码组织
  • 方便后期维护和修改
  • 每个 Builder 可以独立测试

6.2 颜色与 Emoji 的使用

这个项目大量使用 Emoji 作为图标,好处是:

  • 零资源依赖,不需要导入图片
  • 自动适配系统深色/浅色模式
  • 减少包体积
  • 开发效率高

6.3 响应式状态管理

项目的页面都用 @State 管理数据:

  • aboutToAppear 中初始化数据
  • 给数组或对象赋值时,ArkTS 会自动触发 UI 更新
  • 不需要手动调用 setState() 或类似方法

七、预览与运行

在 DevEco Studio 中,可以通过 Previewer 快速预览页面效果:

  1. 打开 Index.ets
  2. 点击右上角的 Previewer 标签
  3. 选择手机设备进行预览

如果需要真机运行,连接鸿蒙手机或使用模拟器,点击 Run 按钮。


总结

本篇我们完成了:

  1. ✅ 鸿蒙 Stage 模型项目的创建和目录理解
  2. ✅ 路由注册和页面跳转
  3. ✅ 首页 8 个 UI 模块的开发
  4. @Builder 装饰器的使用
  5. ✅ 资源文件的配置

下一篇我们将开发 装备库页面,实现分类筛选、装备列表、重量统计、维护提醒等核心功能。
在这里插入图片描述


项目信息:API 23 (compatible) / API 24 (target) | Stage 模型 | ArkTS

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐