《鸿蒙原生应用开发实战》第五篇:收藏功能、资源管理与构建发布

前言

经过前四篇的开发,我们的「光遇·心境」应用已经有了完整的框架、数据模型、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 包优化建议

  1. 移除未使用的资源:检查 rawfile/media/ 中是否有未使用的图片
  2. 代码混淆:Release 构建时启用混淆(obfuscation.enable: true
  3. 缩减包体积
    • 图片用 WebP 格式替代 PNG
    • 移除调试日志
    • 移除 ohosTest 模块

发布检查清单

  • bundleName 已改为正式包名(非 com.example.xxx)
  • versionCode 和 versionName 已更新
  • 签名配置已完成(.p12 / .csr / .cer / .p7b
  • 应用名称和图标已替换为正式资源
  • 已测试过 Release 构建
  • 已适配不同分辨率设备
  • 隐私权限声明完整

六、项目总结与迭代方向

已完成功能

「光遇·心境」应用 v1.0.0
├── 首页(分类入口 + 每日推荐 + 精选推荐横向滚动)
├── 场景探索列表(5 分类标签筛选 + 网格卡片)
├── 场景详情页(沉浸渐变背景 + 色彩分析 + 白噪音推荐)
├── 我的收藏(收藏列表 + 取消收藏 + 空状态)
└── 个人中心(旅行统计 + 功能菜单 + 设置)

可迭代方向

  1. 白噪音播放器:集成 Audio 模块,在查看场景时播放对应白噪音
  2. 动画过渡:页面切换时添加共享元素过渡动画
  3. 字体切换:支持更多中文字体选择
  4. 场景编辑:允许用户自定义场景的色彩和描述
  5. 云同步:收藏列表跨设备同步(使用云数据库)
  6. Widget 卡片:桌面小组件显示每日推荐
  7. 动效增强:粒子系统模拟晨光、星光等动态效果

七、踩坑记录

坑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! 🚀

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐