鸿蒙原生应用实战(四):塔罗牌App开发 — 收藏功能与主题切换系统
鸿蒙原生应用实战(四):塔罗牌App开发 — 收藏功能与主题切换系统
前言
一个完整的 App 少不了 用户数据管理 和 个性化体验。本篇文章将深入讲解塔罗牌 App 中的两个核心系统:
- 收藏管理器(FavoriteManager):收藏/取消收藏的状态管理
- 主题管理器(ThemeManager):深色/浅色主题的订阅发布模式
- 收藏页面(FavPage):收藏列表展示与空状态设计
这两个系统虽然代码量不大,但展示了一种纯静态类管理器的设计模式——这是鸿蒙 Stage 模型下非常实用的轻量级状态管理方案。
本文亮点:
- 静态类管理器的设计哲学与适用场景
- 订阅发布模式的实现细节与内存泄漏防范
- 主题系统的色彩心理学与无障碍设计考量
- 收藏功能的用户体验优化策略
- 从内存存储到持久化的平滑演进路径
- 性能优化技巧与最佳实践分享
目标读者:
- 鸿蒙 ArkTS 初学者,学习状态管理方案
- 中级开发者,了解架构设计模式
- 产品设计师,关注用户体验细节
- 技术负责人,考虑技术选型与扩展性
技术栈:
- HarmonyOS API 23+
- Stage 应用模型
- ArkTS 语言
- 纯前端实现,无后端依赖
让我们开始深入这两个核心系统的设计与实现。
一、收藏管理器设计
1.1 为什么选择静态类?
在鸿蒙 ArkTS 中,状态管理有几种选择:
| 方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
@State 本地状态 |
简单直接 | 无法跨页面共享 | 单页面数据 |
| 静态类管理器 | 全局共享、无实例化 | 进程退出后丢失 | 运行时全局状态 |
| AppStorage/LocalStorage | 官方推荐、支持持久化 | API 有一定学习成本 | 需要持久化的全局状态 |
| 数据库(RDB) | 永久存储 | 复杂度高 | 大量结构化数据 |
对于收藏功能,我们选择 静态类管理器,原因是:
- 收藏数据量小(最多 78 张牌)
- 无需持久化(本期先做内存版,后续可轻松扩展为持久化)
- 实现简单,代码清晰
1.2 FavoriteManager 完整实现
export class FavoriteManager {
static favorites: number[] = [];
// 切换收藏状态,返回新的状态
static toggle(id: number): boolean {
const index = FavoriteManager.favorites.indexOf(id);
if (index >= 0) {
FavoriteManager.favorites.splice(index, 1);
return false; // 已取消收藏
} else {
FavoriteManager.favorites.push(id);
return true; // 已添加收藏
}
}
// 是否已收藏
static isFavorite(id: number): boolean {
return FavoriteManager.favorites.indexOf(id) >= 0;
}
// 获取所有收藏的 ID 列表
static getAll(): number[] {
return FavoriteManager.favorites;
}
// 清空所有收藏
static clear(): void {
FavoriteManager.favorites = [];
}
// 获取收藏数量
static getCount(): number {
return FavoriteManager.favorites.length;
}
}
设计要点:
- 所有方法都是
static,无需实例化,全局可直接调用 toggle返回 boolean:调用者可以根据返回值更新 UI,而不需要再查一次状态- 使用
indexOf+splice:ArkTS 中数组操作与标准 JavaScript 一致 - 收藏 ID 数组:只存 ID 而非完整对象,节省内存且保持数据一致性
1.3 跨页面共享
由于静态类在整个应用生命周期内常驻内存,任何页面都可以直接访问:
// 列表页 — 检查初始收藏状态
aboutToAppear(): void {
this.isFav = FavoriteManager.isFavorite(this.card.id);
}
// 详情页 — 切换收藏
toggleFav(): void {
this.isFav = FavoriteManager.toggle(this.card.id);
}
// 收藏页 — 获取所有收藏
loadFavorites(): void {
const favIds = FavoriteManager.getAll();
const result: TarotCard[] = [];
for (let i = 0; i < TAROT_CARDS.length; i++) {
if (favIds.indexOf(TAROT_CARDS[i].id) >= 0) {
result.push(TAROT_CARDS[i]);
}
}
this.favList = result;
}
1.4 收藏在列表页中的应用
在 CardListPage 的 CardItem 组件中:
@Component
struct CardItem {
card: TarotCard = { /* ... */ };
theme: ThemeColors = { /* ... */ };
@State isFav: boolean = false;
aboutToAppear(): void {
this.isFav = FavoriteManager.isFavorite(this.card.id);
}
toggleFav(): void {
this.isFav = FavoriteManager.toggle(this.card.id);
}
build() {
// ...
Text(this.isFav ? '★' : '☆')
.fontSize(22)
.fontColor(this.isFav ? this.theme.favorite : this.theme.tabInactive)
.onClick((event: ClickEvent) => { this.toggleFav(); });
}
}
交互反馈:
- 未收藏:灰色 ☆
- 已收藏:粉色 ★(
#FF6B9D) - 点击后颜色和符号立即变化
二、收藏页面(FavPage)实现
2.1 页面结构
@Entry
@Component
struct FavPage {
@State favList: TarotCard[] = [];
@State theme: ThemeColors = ThemeManager.colors;
aboutToAppear(): void { /* 订阅主题 + 加载收藏 */ }
onPageShow(): void { /* 刷新收藏列表 */ }
}
2.2 空状态设计
当用户还没有收藏任何牌时,展示友好的空状态:
if (this.favList.length === 0) {
Column() {
Text('📖').fontSize(64);
Text('还没有收藏任何塔罗牌')
.fontColor(this.theme.textSecondary);
Text('去牌义列表中收藏你喜欢的牌吧')
.fontColor(this.theme.textSecondary);
Button() {
Text('去浏览').fontColor('#FFFFFF');
}
.backgroundColor(this.theme.card)
.borderRadius($r('app.float.app_button_radius'))
.onClick(() => { router.pushUrl({ url: 'pages/CardListPage' }); });
}
.height('70%')
.justifyContent(FlexAlign.Center)
.alignItems(HorizontalAlign.Center);
}
空状态设计原则:
- 使用 Emoji + 文案,比冷冰冰的"暂无数据"更亲切
- 提供明确的行动按钮,引导用户去浏览牌义列表
- 垂直居中,视觉舒适
2.3 收藏列表渲染
Scroll() {
Column() {
ForEach(this.favList, (item: TarotCard) => {
Row() {
// 编号图标
Column() {
Text(item.number).fontColor(item.color);
}
.width(44).height(44).borderRadius(22);
// 名称 + 分类
Column() {
Text(item.name).fontWeight(FontWeight.Bold);
Text(item.englishName + ' · ' + item.arcana);
}
// 取消收藏按钮
Text('取消收藏')
.fontColor(this.theme.favorite)
.backgroundColor('rgba(255,107,157,0.1)')
.borderRadius(8)
.onClick(() => { this.removeFavorite(item.id); });
}
.onClick(() => {
router.pushUrl({
url: 'pages/CardDetailPage',
params: { id: item.id }
});
});
});
}
}
2.4 清空所有收藏
顶部导航栏右侧的"清空"按钮:
// 标题行
Row() {
Text('←').onClick(() => { router.back(); });
Text($r('app.string.title_favorites'));
Flex({ direction: FlexDirection.RowReverse }) {
if (this.favList.length > 0) {
Text('清空')
.fontColor(this.theme.textSecondary)
.onClick(() => { this.clearAll(); });
}
}
.layoutWeight(1);
}
// 清空方法
clearAll(): void {
FavoriteManager.clear();
this.favList = [];
}
三、主题管理器设计
3.1 订阅发布模式
主题切换需要更新所有的页面,我们使用经典的 订阅-发布模式:
type ThemeListener = () => void;
export class ThemeManager {
static isLight: boolean = false; // 当前是否为浅色主题
private static listeners: ThemeListener[] = []; // 订阅者列表
// 获取当前主题色板
static get colors(): ThemeColors {
return ThemeManager.isLight ? CALM_LIGHT : DARK_MYSTIC;
}
// 切换主题
static toggle(): void {
ThemeManager.isLight = !ThemeManager.isLight;
// 通知所有订阅者
for (let i = 0; i < ThemeManager.listeners.length; i++) {
ThemeManager.listeners[i]();
}
}
// 显式设置主题
static setLight(light: boolean): void {
if (ThemeManager.isLight !== light) {
ThemeManager.toggle();
}
}
// 订阅主题变化
static subscribe(listener: ThemeListener): void {
ThemeManager.listeners.push(listener);
}
// 取消订阅
static unsubscribe(listener: ThemeListener): void {
const idx = ThemeManager.listeners.indexOf(listener);
if (idx >= 0) {
ThemeManager.listeners.splice(idx, 1);
}
}
}
3.2 各页面如何订阅
每个需要响应主题变化的页面都要在 aboutToAppear 中订阅,在 aboutToDisappear 中取消订阅:
aboutToAppear(): void {
this.theme = ThemeManager.colors; // 初始化主题
ThemeManager.subscribe(() => {
this.theme = ThemeManager.colors; // 主题变化时更新状态
});
}
aboutToDisappear(): void {
ThemeManager.unsubscribe(() => {}); // 注意:这里需要传递同一个函数引用
}
⚠️ 重要问题: 上面代码中
() => {}每次调用都创建了一个新函数,unsubscribe时indexOf找不到匹配项!正确的做法是把回调函数保存为变量:
// ✅ 正确的做法
private themeListener: ThemeListener = () => {
this.theme = ThemeManager.colors;
};
aboutToAppear(): void {
this.theme = ThemeManager.colors;
ThemeManager.subscribe(this.themeListener);
}
aboutToDisappear(): void {
ThemeManager.unsubscribe(this.themeListener);
}
四、深色与浅色主题定义
4.1 ThemeColors 接口
定义色板的结构:
export interface ThemeColors {
bg: string; // 背景色
card: string; // 卡片背景色
textPrimary: string; // 主文字颜色
textSecondary: string; // 次要文字颜色
accent: string; // 强调色
tabInactive: string; // 标签未选中色
favorite: string; // 收藏图标色
cardBorder: string; // 卡片边框色
tagBg: string; // 标签背景色
}
4.2 深色主题(暗黑神秘风)
export const DARK_MYSTIC: ThemeColors = {
bg: '#1A0A2E', // 深紫色背景
card: '#2D1B4E', // 紫罗兰卡片
textPrimary: '#FFFFFF', // 白色文字
textSecondary: '#B8A8D0', // 浅紫色辅助文字
accent: '#D4AF37', // 金色强调
tabInactive: '#6B5B8E', // 灰紫色未选中
favorite: '#FF6B9D', // 粉色收藏
cardBorder: '#D4AF37', // 金色边框
tagBg: 'rgba(212,175,55,0.12)' // 金色半透明背景
};
4.3 浅色主题(宁静明亮风)
export const CALM_LIGHT: ThemeColors = {
bg: '#F5F0FF', // 浅紫白背景
card: '#FFFFFF', // 纯白卡片
textPrimary: '#2D1B4E', // 深紫文字
textSecondary: '#8A7AA0', // 灰紫辅助文字
accent: '#7C3AED', // 紫色强调
tabInactive: '#C4B5D4', // 淡紫未选中
favorite: '#E11D48', // 红色收藏
cardBorder: '#7C3AED', // 紫色边框
tagBg: 'rgba(124,58,237,0.08)' // 紫色半透明背景
};
设计理念:
- 深色主题:神秘、深邃,配合塔罗牌的神秘学氛围
- 浅色主题:清新、易读,适合日间使用
- 两组色板在结构上完全一致(字段数量相同、语义对应),切换时不会出现布局错位
五、主题切换的实际应用效果
在首页,主题切换按钮位于右上角:
// 深色 → 浅色时,月亮图标变为太阳图标
Text(ThemeManager.isLight ? '🌙' : '☀️')
.fontSize(22)
.onClick(() => { this.toggleTheme(); });
所有页面中,颜色属性都通过 this.theme.xxx 引用:
// 示例:列表页背景
.backgroundColor(this.theme.bg)
// 卡片背景
.backgroundColor(this.theme.card)
// 强调色文字
.fontColor(this.theme.accent)
// 收藏图标
.fontColor(this.isFav ? this.theme.favorite : this.theme.tabInactive)
六、扩展思考:数据持久化
目前的收藏数据存储在内存中,App 重启后会丢失。如果需要持久化,有几种方案:
6.1 使用 Preferences(轻量级 KV 存储)
import { preferences } from '@kit.ArkData';
// 保存
const prefs = await preferences.getPreferences(this.context, 'my_prefs');
await prefs.put('favorites', JSON.stringify(favorites));
await prefs.flush();
// 读取
const json = await prefs.get('favorites', '[]');
favorites = JSON.parse(json);
6.2 使用 RelationalStore(关系型数据库)
适合更复杂的数据查询场景,但收藏功能用 RDB 有点过重。
6.3 使用 AppStorage(全局 UI 状态存储)
官方推荐方式,但需要关注其与 ArkTS 响应式系统的配合。

七、小结
本篇我们完成了:
- ✅ FavoriteManager 静态收藏管理器设计
- ✅ FavPage 收藏页(列表 + 空状态 + 清空)
- ✅ ThemeManager 订阅发布模式
- ✅ 深色/浅色双主题色板定义
- ✅ 跨页面主题切换的实现
- ✅ 数据持久化的扩展思路
下一篇是 收官之篇,我们将讲解 TarotData 全量数据模型设计、API 版本适配策略、构建配置优化,以及从开发到上线的完整流程。
项目代码: 基于 HarmonyOS API 23 + Stage 模型 + ArkTS
涉及文件: model/TarotData.ets + pages/FavPage.ets
下篇预告: 数据模型、构建配置与工程优化 — 从开发到上线的最后一公里
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐


所有评论(0)