鸿蒙原生应用实战四塔罗牌App开发 — 收藏功能与主题切换系统前言一个完整的 App 少不了用户数据管理和个性化体验。本篇文章将深入讲解塔罗牌 App 中的两个核心系统收藏管理器FavoriteManager收藏/取消收藏的状态管理主题管理器ThemeManager深色/浅色主题的订阅发布模式收藏页面FavPage收藏列表展示与空状态设计这两个系统虽然代码量不大但展示了一种纯静态类管理器的设计模式——这是鸿蒙 Stage 模型下非常实用的轻量级状态管理方案。本文亮点静态类管理器的设计哲学与适用场景订阅发布模式的实现细节与内存泄漏防范主题系统的色彩心理学与无障碍设计考量收藏功能的用户体验优化策略从内存存储到持久化的平滑演进路径性能优化技巧与最佳实践分享目标读者鸿蒙 ArkTS 初学者学习状态管理方案中级开发者了解架构设计模式产品设计师关注用户体验细节技术负责人考虑技术选型与扩展性技术栈HarmonyOS API 23Stage 应用模型ArkTS 语言纯前端实现无后端依赖让我们开始深入这两个核心系统的设计与实现。一、收藏管理器设计1.1 为什么选择静态类在鸿蒙 ArkTS 中状态管理有几种选择方案优点缺点适用场景State本地状态简单直接无法跨页面共享单页面数据静态类管理器全局共享、无实例化进程退出后丢失运行时全局状态AppStorage/LocalStorage官方推荐、支持持久化API 有一定学习成本需要持久化的全局状态数据库RDB永久存储复杂度高大量结构化数据对于收藏功能我们选择静态类管理器原因是收藏数据量小最多 78 张牌无需持久化本期先做内存版后续可轻松扩展为持久化实现简单代码清晰1.2 FavoriteManager 完整实现exportclassFavoriteManager{staticfavorites:number[][];// 切换收藏状态返回新的状态statictoggle(id:number):boolean{constindexFavoriteManager.favorites.indexOf(id);if(index0){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而不需要再查一次状态使用indexOfspliceArkTS 中数组操作与标准 JavaScript 一致收藏 ID 数组只存 ID 而非完整对象节省内存且保持数据一致性1.3 跨页面共享由于静态类在整个应用生命周期内常驻内存任何页面都可以直接访问// 列表页 — 检查初始收藏状态aboutToAppear():void{this.isFavFavoriteManager.isFavorite(this.card.id);}// 详情页 — 切换收藏toggleFav():void{this.isFavFavoriteManager.toggle(this.card.id);}// 收藏页 — 获取所有收藏loadFavorites():void{constfavIdsFavoriteManager.getAll();constresult:TarotCard[][];for(leti0;iTAROT_CARDS.length;i){if(favIds.indexOf(TAROT_CARDS[i].id)0){result.push(TAROT_CARDS[i]);}}this.favListresult;}1.4 收藏在列表页中的应用在 CardListPage 的 CardItem 组件中Componentstruct CardItem{card:TarotCard{/* ... */};theme:ThemeColors{/* ... */};StateisFav:booleanfalse;aboutToAppear():void{this.isFavFavoriteManager.isFavorite(this.card.id);}toggleFav():void{this.isFavFavoriteManager.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 页面结构EntryComponentstruct FavPage{StatefavList:TarotCard[][];Statetheme:ThemeColorsThemeManager.colors;aboutToAppear():void{/* 订阅主题 加载收藏 */}onPageShow():void{/* 刷新收藏列表 */}}2.2 空状态设计当用户还没有收藏任何牌时展示友好的空状态if(this.favList.length0){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.length0){Text(清空).fontColor(this.theme.textSecondary).onClick((){this.clearAll();});}}.layoutWeight(1);}// 清空方法clearAll():void{FavoriteManager.clear();this.favList[];}三、主题管理器设计3.1 订阅发布模式主题切换需要更新所有的页面我们使用经典的订阅-发布模式typeThemeListener()void;exportclassThemeManager{staticisLight:booleanfalse;// 当前是否为浅色主题privatestaticlisteners:ThemeListener[][];// 订阅者列表// 获取当前主题色板staticgetcolors():ThemeColors{returnThemeManager.isLight?CALM_LIGHT:DARK_MYSTIC;}// 切换主题statictoggle():void{ThemeManager.isLight!ThemeManager.isLight;// 通知所有订阅者for(leti0;iThemeManager.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{constidxThemeManager.listeners.indexOf(listener);if(idx0){ThemeManager.listeners.splice(idx,1);}}}3.2 各页面如何订阅每个需要响应主题变化的页面都要在aboutToAppear中订阅在aboutToDisappear中取消订阅aboutToAppear():void{this.themeThemeManager.colors;// 初始化主题ThemeManager.subscribe((){this.themeThemeManager.colors;// 主题变化时更新状态});}aboutToDisappear():void{ThemeManager.unsubscribe((){});// 注意这里需要传递同一个函数引用}⚠️ 重要问题: 上面代码中() {}每次调用都创建了一个新函数unsubscribe时indexOf找不到匹配项正确的做法是把回调函数保存为变量// ✅ 正确的做法privatethemeListener:ThemeListener(){this.themeThemeManager.colors;};aboutToAppear():void{this.themeThemeManager.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}fromkit.ArkData;// 保存constprefsawaitpreferences.getPreferences(this.context,my_prefs);awaitprefs.put(favorites,JSON.stringify(favorites));awaitprefs.flush();// 读取constjsonawaitprefs.get(favorites,[]);favoritesJSON.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下篇预告: 数据模型、构建配置与工程优化 — 从开发到上线的最后一公里