《鸿蒙原生应用开发实战》第五篇:收藏功能、资源管理与构建发布
《鸿蒙原生应用开发实战》第五篇:收藏功能、资源管理与构建发布
前言
经过前四篇的开发,我们的「光遇·心境」应用已经有了完整的框架、数据模型、UI 设计和页面导航。本篇将完成最后的功能闭环 —— 收藏功能的完整实现、资源管理的规范、以及如何将应用构建发布为 HAP 包。
本文将涵盖:
- 收藏功能的完整实现(3 个页面的联动)
- AppStorage 在收藏功能中的应用
- 资源文件管理规范(string/color/float)
- 构建配置详解
- HAP 包构建与优化
- 项目总结与迭代方向
一、收藏功能完整实现
收藏功能涉及 3 个页面的联动:首页(进入)、详情页(切换收藏)、收藏页(展示和管理)。我们一步步拆解。
1.1 数据层面设计
// model/SceneData.ets// AppStorage keyexportconstFAV_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)
详情页是用户执行收藏/取消收藏操作的地方:
@Componentstruct DetailPage{@StateisFav:boolean=false;// 页面出现时检查收藏状态aboutToAppear():void{constparams=router.getParams()asRecord<string,Object>;if(params&¶ms['sceneId']){constid=params['sceneId']asnumber;this.scene=getSceneById(id);if(this.scene){this.checkFavStatus();}}}// 从 AppStorage 读取收藏列表,判断当前场景是否在列表中checkFavStatus():void{constfavList:number[]=AppStorage.get<number[]>(FAV_KEY)||[];this.isFav=this.scene?favList.indexOf(this.scene.id)>=0:false;}// 切换收藏状态toggleFav():void{if(!this.scene)return;letfavList:number[]=AppStorage.get<number[]>(FAV_KEY)||[];constidx=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)
@Componentstruct FavPage{@StatefavScenes:SceneItem[]=[];@StatefavCount:number=0;// 每次进入页面时重新加载收藏列表aboutToAppear():void{this.loadFavScenes();}// 从 AppStorage 读取所有收藏 ID,组装成 SceneItem 数组loadFavScenes():void{constfavIds:number[]=AppStorage.get<number[]>(FAV_KEY)||[];constlist:SceneItem[]=[];for(constidoffavIds){constscene=getSceneById(id);if(scene){list.push(scene);}}this.favScenes=list;this.favCount=list.length;}// 取消收藏removeFav(sceneId:number):void{letfavList:number[]=AppStorage.get<number[]>(FAV_KEY)||[];constidx=favList.indexOf(sceneId);if(idx>=0){favList.splice(idx,1);AppStorage.set<number[]>(FAV_KEY,favList);this.loadFavScenes();// 重新加载列表}}}收藏列表的卡片渲染:
@BuilderFavCard(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{constfavIds: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'))// 24vp2.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--modemodule\-pmodule=entry@default\-pproduct=default\-prequiredDeviceType=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';constDOMAIN=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,()=>{constscene=getSceneById(1);expect(scene).not().undefined();expect(scene.name).assertEqual('黎明破晓');});it('getScenesByCategory_should_filter_correctly',0,()=>{constscenes=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! 🚀
