news 2026/6/15 8:15:56

鸿蒙原生应用实战(四):塔罗牌App开发 — 收藏功能与主题切换系统

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
鸿蒙原生应用实战(四):塔罗牌App开发 — 收藏功能与主题切换系统

鸿蒙原生应用实战(四):塔罗牌App开发 — 收藏功能与主题切换系统

前言

一个完整的 App 少不了用户数据管理个性化体验。本篇文章将深入讲解塔罗牌 App 中的两个核心系统:

  1. 收藏管理器(FavoriteManager):收藏/取消收藏的状态管理
  2. 主题管理器(ThemeManager):深色/浅色主题的订阅发布模式
  3. 收藏页面(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);}

空状态设计原则

  1. 使用 Emoji + 文案,比冷冰冰的"暂无数据"更亲切
  2. 提供明确的行动按钮,引导用户去浏览牌义列表
  3. 垂直居中,视觉舒适

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(()=>{});// 注意:这里需要传递同一个函数引用}

⚠️ 重要问题: 上面代码中() => {}每次调用都创建了一个新函数,unsubscribeindexOf找不到匹配项!正确的做法是把回调函数保存为变量:

// ✅ 正确的做法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 响应式系统的配合。


七、小结

本篇我们完成了:

  1. ✅ FavoriteManager 静态收藏管理器设计
  2. ✅ FavPage 收藏页(列表 + 空状态 + 清空)
  3. ✅ ThemeManager 订阅发布模式
  4. ✅ 深色/浅色双主题色板定义
  5. ✅ 跨页面主题切换的实现
  6. ✅ 数据持久化的扩展思路

下一篇是收官之篇,我们将讲解 TarotData 全量数据模型设计、API 版本适配策略、构建配置优化,以及从开发到上线的完整流程。

项目代码: 基于 HarmonyOS API 23 + Stage 模型 + ArkTS
涉及文件: model/TarotData.ets + pages/FavPage.ets
下篇预告: 数据模型、构建配置与工程优化 — 从开发到上线的最后一公里

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/6/15 8:12:19

别再瞎测了!用LCR电桥测同轴电缆参数,这3个坑我帮你踩过了

别再瞎测了&#xff01;用LCR电桥测同轴电缆参数&#xff0c;这3个坑我帮你踩过了在射频工程和电子调试领域&#xff0c;同轴电缆参数的准确测量常常成为项目成败的关键。许多工程师都曾遇到过这样的困惑&#xff1a;为什么用LCR电桥测量同轴电缆时&#xff0c;低频下会显示10H…

作者头像 李华
网站建设 2026/6/15 7:44:55

跨模态对齐技术:SIGROT方法解析与实践

1. 跨模态对齐的挑战与现状跨模态学习作为连接视觉与语言的重要桥梁&#xff0c;其核心难题在于如何弥合不同模态间的语义鸿沟。想象一下&#xff0c;当人类看到一张"夕阳下的河畔"照片时&#xff0c;大脑能瞬间联想到对应的文字描述&#xff0c;这种跨模态的语义关联…

作者头像 李华
网站建设 2026/6/15 7:38:54

告别命令行!2024年我用这三款免费GUI工具管理PostgreSQL,效率翻倍

2024年PostgreSQL图形化管理工具实战指南&#xff1a;DBeaver、pgAdmin与Beekeeper Studio深度测评对于许多开发者而言&#xff0c;PostgreSQL的命令行操作就像一堵无形的墙——功能强大却令人望而生畏。我曾见过团队里的数据分析师因为一个简单的表连接查询而反复查阅psql手册…

作者头像 李华