《鸿蒙原生应用开发实战》第五篇:收藏功能、资源管理与构建发布
《鸿蒙原生应用开发实战》第五篇:收藏功能、资源管理与构建发布
前言
经过前四篇的开发,我们的「光遇·心境」应用已经有了完整的框架、数据模型、UI 设计和页面导航。本篇将完成最后的功能闭环 —— 收藏功能的完整实现、资源管理的规范、以及如何将应用构建发布为 HAP 包。
本文将涵盖:
- 收藏功能的完整实现(3 个页面的联动)
- AppStorage 在收藏功能中的应用
- 资源文件管理规范(string/color/float)
- 构建配置详解
- HAP 包构建与优化
- 项目总结与迭代方向
一、收藏功能完整实现
收藏功能涉及 3 个页面的联动:首页(进入)、详情页(切换收藏)、收藏页(展示和管理)。我们一步步拆解。
1.1 数据层面设计
// model/SceneData.ets
// AppStorage key
export const FAV_KEY: string = 'fav_scenes';
收藏的数据结构非常简单:用 AppStorage 存储一个 number[] 数组,每个元素是被收藏场景的 id。
1.2 首页初始化(Index.ets)
aboutToAppear(): void {
// 确保 AppStorage 中的收藏列表已初始化
if (!AppStorage.has(FAV_KEY)) {
AppStorage.set<number[]>(FAV_KEY, []);
}
}
这一步很关键 —— 在其他页面读取 FAV_KEY 之前,确保 key 已存在于 AppStorage 中,否则 AppStorage.get() 会返回 undefined。
1.3 详情页收藏切换(DetailPage.ets)
详情页是用户执行收藏/取消收藏操作的地方:
@Component
struct DetailPage {
@State isFav: boolean = false;
// 页面出现时检查收藏状态
aboutToAppear(): void {
const params = router.getParams() as Record<string, Object>;
if (params && params['sceneId']) {
const id = params['sceneId'] as number;
this.scene = getSceneById(id);
if (this.scene) {
this.checkFavStatus();
}
}
}
// 从 AppStorage 读取收藏列表,判断当前场景是否在列表中
checkFavStatus(): void {
const favList: number[] = AppStorage.get<number[]>(FAV_KEY) || [];
this.isFav = this.scene ? favList.indexOf(this.scene.id) >= 0 : false;
}
// 切换收藏状态
toggleFav(): void {
if (!this.scene) return;
let favList: number[] = AppStorage.get<number[]>(FAV_KEY) || [];
const idx = favList.indexOf(this.scene.id);
if (idx >= 0) {
favList.splice(idx, 1); // 取消收藏
this.isFav = false;
} else {
favList.push(this.scene.id); // 添加收藏
this.isFav = true;
}
// 写回 AppStorage(必须重新 set 才能触发更新)
AppStorage.set<number[]>(FAV_KEY, favList);
}
}
UI 上,收藏按钮的显示状态动态跟随 @State isFav:
// 收藏按钮
Text(this.isFav ? '❤️' : '🤍')
.fontSize(26)
.onClick(() => {
this.toggleFav();
})
1.4 收藏页展示与管理(FavPage.ets)
@Component
struct FavPage {
@State favScenes: SceneItem[] = [];
@State favCount: number = 0;
// 每次进入页面时重新加载收藏列表
aboutToAppear(): void {
this.loadFavScenes();
}
// 从 AppStorage 读取所有收藏 ID,组装成 SceneItem 数组
loadFavScenes(): void {
const favIds: number[] = AppStorage.get<number[]>(FAV_KEY) || [];
const list: SceneItem[] = [];
for (const id of favIds) {
const scene = getSceneById(id);
if (scene) {
list.push(scene);
}
}
this.favScenes = list;
this.favCount = list.length;
}
// 取消收藏
removeFav(sceneId: number): void {
let favList: number[] = AppStorage.get<number[]>(FAV_KEY) || [];
const idx = favList.indexOf(sceneId);
if (idx >= 0) {
favList.splice(idx, 1);
AppStorage.set<number[]>(FAV_KEY, favList);
this.loadFavScenes(); // 重新加载列表
}
}
}
收藏列表的卡片渲染:
@Builder
FavCard(item: SceneItem) {
Row() {
// 左侧彩色竖条(渐变色)
Row()
.width(6).height('100%').borderRadius(3)
.linearGradient({
direction: GradientDirection.Bottom,
colors: [[item.colors[0], 0], [item.colors[2], 1]]
})
.margin({ right: 14 })
// 中间:名称 + 描述 + 标签
Column() {
Text(item.name).fontSize(18).fontColor(Color.White).fontWeight(FontWeight.Bold)
Text(item.desc).fontSize(12).fontColor($r('app.color.text_secondary'))
Row() {
Text(item.category).fontSize(10)
Text(`${item.duration} 分钟`).fontSize(10)
}
}
.layoutWeight(1)
// 右侧:取消收藏按钮
Column() {
Text('❤️').fontSize(22)
.onClick(() => { this.removeFav(item.id); })
Text($r('app.string.cancel_fav')).fontSize(9)
}
}
}
1.5 个人中心展示收藏数(ProfilePage.ets)
aboutToAppear(): void {
const favIds: number[] = AppStorage.get<number[]>(FAV_KEY) || [];
this.favCount = favIds.length;
}
1.6 数据流总结
点击收藏 ❤️
↓
DetailPage.toggleFav()
├── 读取 AppStorage(FAV_KEY) → number[]
├── 添加或移除当前 sceneId
└── 写回 AppStorage(FAV_KEY) → 触发全局更新
↓
FavPage.aboutToAppear() ProfilePage.aboutToAppear()
↓ ↓
loadFavScenes() 读取 favCount
↓ ↓
渲染收藏列表 显示统计数字
设计的巧妙之处:使用 AppStorage 作为数据总线,各页面在
aboutToAppear中读取最新数据,不需要复杂的观察者模式或事件总线。
二、资源文件管理规范
2.1 三资源体系
| 资源类型 | 文件名 | 用途 |
|---|---|---|
| 字符串 | string.json |
所有用户可见文本 |
| 颜色 | color.json |
主题色、文字色、背景色 |
| 尺寸 | float.json |
字号、间距、圆角 |
2.2 字符串资源(string.json)
{
"string": [
{ "name": "index_title", "value": "今日光感" },
{ "name": "index_subtitle", "value": "用光影治愈心灵" },
{ "name": "my_fav", "value": "我的收藏" },
{ "name": "fav_empty", "value": "还没有收藏的场景" },
{ "name": "fav_empty_desc", "value": "去探索页面发现喜欢的光影场景吧" },
{ "name": "color_analysis", "value": "色彩分析" },
{ "name": "recommend_sound", "value": "推荐白噪音" },
{ "name": "cancel_fav", "value": "取消收藏" },
{ "name": "scene_explore", "value": "场景探索" },
{ "name": "scene_explore_desc", "value": "发现属于你的光影世界" }
]
}
命名规范:
- 按功能模块加前缀:
index_、fav_、scene_ - 使用小写字母 + 下划线
使用方式:
Text($r('app.string.my_fav')) // → "我的收藏"
2.3 颜色资源(color.json)
{
"color": [
{ "name": "start_window_background", "value": "#1a1a2e" },
{ "name": "text_primary", "value": "#FFFFFF" },
{ "name": "text_secondary", "value": "#B0B0C0" },
{ "name": "text_accent", "value": "#FFD700" },
{ "name": "fav_active", "value": "#FF4757" },
{ "name": "fav_inactive", "value": "#88FFFFFF" }
]
}
使用方式:
.fontColor($r('app.color.text_secondary'))
.backgroundColor($r('app.color.card_bg'))
2.4 尺寸资源(float.json)
{
"float": [
{ "name": "title_font_size", "value": "28fp" },
{ "name": "subtitle_font_size", "value": "16fp" },
{ "name": "body_font_size", "value": "14fp" },
{ "name": "caption_font_size", "value": "12fp" },
{ "name": "card_radius", "value": "16vp" },
{ "name": "large_radius", "value": "24vp" },
{ "name": "padding_small", "value": "8vp" },
{ "name": "padding_medium", "value": "16vp" },
{ "name": "padding_large", "value": "24vp" }
]
}
使用方式:
.fontSize($r('app.float.body_font_size')) // 14fp
.borderRadius($r('app.float.card_radius')) // 16vp
.padding($r('app.float.padding_large')) // 24vp
2.5 深浅色适配
支持深色模式,在 resources/dark/element/ 中定义深色专有颜色:
resources/
├── base/element/color.json # 默认(浅色或基础值)
└── dark/element/color.json # 深色模式覆盖值
三、构建配置详解
3.1 项目级 build-profile.json5
{
"app": {
"products": [
{
"name": "default",
"signingConfig": "default",
"targetSdkVersion": "6.1.1(24)",
"compatibleSdkVersion": "6.1.0(23)",
"runtimeOS": "HarmonyOS",
"buildOption": {
"strictMode": {
"caseSensitiveCheck": true,
"useNormalizedOHMUrl": true
}
}
}
]
},
"modules": [
{
"name": "entry",
"srcPath": "./entry",
"targets": [{"name": "default", "applyToProducts": ["default"]}]
}
]
}
关键配置解释:
compatibleSdkVersion: 23:最低支持 API 23 的设备targetSdkVersion: 24:目标 SDK 版本caseSensitiveCheck: true:启用大小写检查(ArkTS 严格模式)useNormalizedOHMUrl: true:规范化模块 URL 引用
3.2 模块级 build-profile.json5
{
"apiType": "stageMode",
"buildOptionSet": [
{
"name": "release",
"arkOptions": {
"obfuscation": {
"ruleOptions": { "enable": false },
"files": ["./obfuscation-rules.txt"]
}
}
}
]
}
3.3 构建命令解析
hvigorw --mode module \
-p module=entry@default \
-p product=default \
-p requiredDeviceType=phone \
assembleHap \
--analyze=normal \
--parallel \
--incremental \
--daemon
| 参数 | 说明 |
|---|---|
--mode module |
模块级构建 |
-p module=entry@default |
构建 entry 模块的 default 分发包 |
-p product=default |
default 产品类型 |
-p requiredDeviceType=phone |
目标设备类型 |
assembleHap |
生成 HAP 包 |
--analyze=normal |
代码分析级别 |
--parallel |
并行构建 |
--incremental |
增量构建(仅编译修改的文件) |
--daemon |
守护进程模式(加速后续构建) |
3.4 构建产物目录
build/
├── output/
│ └── default/
│ ├── entry-default-unsigned.hap # 未签名包
│ └── entry-default-signed.hap # 已签名包(发布用)
└── ...
四、调试与测试
4.1 hilog 日志输出
import { hilog } from '@kit.PerformanceAnalysisKit';
const DOMAIN = 0x0000;
// 输出日志
hilog.info(DOMAIN, 'testTag', '页面加载成功: %{public}s', pageName);
hilog.error(DOMAIN, 'testTag', '加载失败: %{public}s', JSON.stringify(err));
日志格式:hilog.级别(域, 标签, 格式化字符串, 参数...)
4.2 DevEco Studio 调试
DevEco Studio 提供了完整的调试工具:
- 断点调试(Breakpoints)
- 变量监视(Watch)
- 调用堆栈(Call Stack)
- Profiler 性能分析
4.3 ohosTest 单元测试
// entry/src/main/ohosTest/ets/test/
import { describe, it, expect } from '@ohos/hypium';
describe('SceneDataTest', () => {
it('getSceneById_should_return_correct_scene', 0, () => {
const scene = getSceneById(1);
expect(scene).not().undefined();
expect(scene.name).assertEqual('黎明破晓');
});
it('getScenesByCategory_should_filter_correctly', 0, () => {
const scenes = getScenesByCategory('海洋');
expect(scenes.length).assertEqual(2);
});
});
五、发布前的检查清单
HAP 包优化建议
- 移除未使用的资源:检查
rawfile/和media/中是否有未使用的图片 - 代码混淆:Release 构建时启用混淆(
obfuscation.enable: true) - 缩减包体积:
- 图片用 WebP 格式替代 PNG
- 移除调试日志
- 移除 ohosTest 模块
发布检查清单
- bundleName 已改为正式包名(非 com.example.xxx)
- versionCode 和 versionName 已更新
- 签名配置已完成(
.p12/.csr/.cer/.p7b) - 应用名称和图标已替换为正式资源
- 已测试过 Release 构建
- 已适配不同分辨率设备
- 隐私权限声明完整
六、项目总结与迭代方向
已完成功能
「光遇·心境」应用 v1.0.0
├── 首页(分类入口 + 每日推荐 + 精选推荐横向滚动)
├── 场景探索列表(5 分类标签筛选 + 网格卡片)
├── 场景详情页(沉浸渐变背景 + 色彩分析 + 白噪音推荐)
├── 我的收藏(收藏列表 + 取消收藏 + 空状态)
└── 个人中心(旅行统计 + 功能菜单 + 设置)
可迭代方向
- 白噪音播放器:集成 Audio 模块,在查看场景时播放对应白噪音
- 动画过渡:页面切换时添加共享元素过渡动画
- 字体切换:支持更多中文字体选择
- 场景编辑:允许用户自定义场景的色彩和描述
- 云同步:收藏列表跨设备同步(使用云数据库)
- Widget 卡片:桌面小组件显示每日推荐
- 动效增强:粒子系统模拟晨光、星光等动态效果
七、踩坑记录
坑1:AppStorage 未初始化导致读取失败
现象:收藏页面进入时报错
原因:FavPage 读取 FAV_KEY 时,该 key 还未在 AppStorage 中注册
解决:在首页的 aboutToAppear 中确保初始化:
if (!AppStorage.has(FAV_KEY)) {
AppStorage.set<number[]>(FAV_KEY, []);
}
坑2:Release 构建失败
现象:assembleHap --mode module 在 release 模式下失败
原因:混淆规则文件 obfuscation-rules.txt 配置了不存在的规则
解决:在 release 构建中暂时禁用混淆,或检查规则文件的语法
坑3:HAP 包安装后闪退
现象:真机安装后打开立即闪退
原因:常见于 manifest 中配置了不存在的 Ability 或资源引用错误
解决:使用 hilog 查看闪退日志,检查 module.json5 中的 srcEntry 路径

连载总结
至此,五篇连载全部完成。我们从零开始构建了一个完整的鸿蒙原生应用,覆盖了从框架搭建到发布的全流程:
| 篇次 | 主题 | 核心内容 |
|---|---|---|
| 第一篇 | 项目框架与路由 | Stage 模型、Ability、路由注册 |
| 第二篇 | 数据模型与状态 | @State、AppStorage、@Builder |
| 第三篇 | 沉浸式 UI 设计 | 渐变、光晕、卡片、布局 |
| 第四篇 | 导航与参数传递 | router API、页面生命周期 |
| 第五篇 | 收藏功能与发布 | 功能闭环、资源管理、构建发布 |
希望通过这五篇文章,你能掌握鸿蒙原生应用开发的核心技能,能够独立构建完整的 Stage 模型应用。鸿蒙生态正在快速发展,现在是最好的入局时机。
Happy coding with HarmonyOS! 🚀
更多推荐



所有评论(0)