鸿蒙原生应用实战(四):塔罗牌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 完整实现
exportclassFavoriteManager{staticfavorites:number[]=[];// 切换收藏状态,返回新的状态statictoggle(id:number):boolean{constindex=FavoriteManager.favorites.indexOf(id);if(index>=0){FavoriteManager.favorites.splice(index,1);returnfalse;// 已取消收藏}else{FavoriteManager.favorites.push(id);returntrue;// 已添加收藏}}// 是否已收藏staticisFavorite(id:number):boolean{returnFavoriteManager.favorites.indexOf(id)>=0;}// 获取所有收藏的 ID 列表staticgetAll():number[]{returnFavoriteManager.favorites;}// 清空所有收藏staticclear():void{FavoriteManager.favorites=[];}// 获取收藏数量staticgetCount():number{returnFavoriteManager.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{constfavIds=FavoriteManager.getAll();constresult:TarotCard[]=[];for(leti=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 组件中:
@Componentstruct CardItem{card:TarotCard={/* ... */};theme:ThemeColors={/* ... */};@StateisFav: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@Componentstruct FavPage{@StatefavList:TarotCard[]=[];@Statetheme: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 订阅发布模式
主题切换需要更新所有的页面,我们使用经典的订阅-发布模式:
typeThemeListener=()=>void;exportclassThemeManager{staticisLight:boolean=false;// 当前是否为浅色主题privatestaticlisteners:ThemeListener[]=[];// 订阅者列表// 获取当前主题色板staticgetcolors():ThemeColors{returnThemeManager.isLight?CALM_LIGHT:DARK_MYSTIC;}// 切换主题statictoggle():void{ThemeManager.isLight=!ThemeManager.isLight;// 通知所有订阅者for(leti=0;i<ThemeManager.listeners.length;i++){ThemeManager.listeners[i]();}}// 显式设置主题staticsetLight(light:boolean):void{if(ThemeManager.isLight!==light){ThemeManager.toggle();}}// 订阅主题变化staticsubscribe(listener:ThemeListener):void{ThemeManager.listeners.push(listener);}// 取消订阅staticunsubscribe(listener:ThemeListener):void{constidx=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找不到匹配项!正确的做法是把回调函数保存为变量:
// ✅ 正确的做法privatethemeListener:ThemeListener=()=>{this.theme=ThemeManager.colors;};aboutToAppear():void{this.theme=ThemeManager.colors;ThemeManager.subscribe(this.themeListener);}aboutToDisappear():void{ThemeManager.unsubscribe(this.themeListener);}四、深色与浅色主题定义
4.1 ThemeColors 接口
定义色板的结构:
exportinterfaceThemeColors{bg:string;// 背景色card:string;// 卡片背景色textPrimary:string;// 主文字颜色textSecondary:string;// 次要文字颜色accent:string;// 强调色tabInactive:string;// 标签未选中色favorite:string;// 收藏图标色cardBorder:string;// 卡片边框色tagBg:string;// 标签背景色}4.2 深色主题(暗黑神秘风)
exportconstDARK_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 浅色主题(宁静明亮风)
exportconstCALM_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';// 保存constprefs=awaitpreferences.getPreferences(this.context,'my_prefs');awaitprefs.put('favorites',JSON.stringify(favorites));awaitprefs.flush();// 读取constjson=awaitprefs.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
下篇预告: 数据模型、构建配置与工程优化 — 从开发到上线的最后一公里