【鸿蒙原生应用实战】第一篇:项目搭建与首页开发——从零构建户外助手App
【鸿蒙原生应用实战】第一篇:项目搭建与首页开发——从零构建户外助手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 的三个参数:
- 数据源数组
- 构建函数
(item, index?) => void - 键值生成器
(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 快速预览页面效果:
- 打开
Index.ets - 点击右上角的 Previewer 标签
- 选择手机设备进行预览
如果需要真机运行,连接鸿蒙手机或使用模拟器,点击 Run 按钮。
总结
本篇我们完成了:
- ✅ 鸿蒙 Stage 模型项目的创建和目录理解
- ✅ 路由注册和页面跳转
- ✅ 首页 8 个 UI 模块的开发
- ✅
@Builder装饰器的使用 - ✅ 资源文件的配置
下一篇我们将开发 装备库页面,实现分类筛选、装备列表、重量统计、维护提醒等核心功能。
项目信息:API 23 (compatible) / API 24 (target) | Stage 模型 | ArkTS
更多推荐




所有评论(0)