《鸿蒙原生应用开发实战》第二篇:ArkTS 数据模型与状态管理
前言
在上一篇中,我们搭建了项目的框架和路由体系。本篇将深入 ArkTS 的数据模型设计和状态管理机制。数据层是一个应用的灵魂,如何组织数据结构、如何管理状态变化、如何在页面之间共享数据,这些都是开发者必须掌握的技能。
本文将涵盖:
- 数据模型定义与接口设计
- 严格模式下的对象字面量规范
- @State、@Builder 装饰器详解
- AppStorage 全局数据共享
- 数据流设计模式
一、数据模型定义
场景数据模型(SceneData.ets)
我们的应用有 8 个沉浸式场景,归属 5 个分类。首先定义SceneItem接口:
// model/SceneData.etsexportinterfaceSceneItem{id:number;// 唯一标识name:string;// 场景名称desc:string;// 简短描述(卡片展示)detail:string;// 详细描述(详情页展示)category:string;// 分类:晨光/森林/海洋/夕阳/星夜colors:string[];// 主题色数组(3个颜色值)sound:string;// 推荐白噪音名称duration:number;// 建议体验时长(分钟)}数据组织
constSCENE_1:SceneItem={id:1,name:'黎明破晓',desc:'金色的曙光穿透云层,唤醒沉睡的大地',detail:'黎明时分,第一缕阳光划破天际,将天空染成金色与绯红交织的画卷...',category:'晨光',colors:['#FF6B35','#F7C948','#FFF4E0'],sound:'清晨鸟鸣',duration:15};// ... 共 8 个场景对象严格模式下的对象字面量陷阱
ArkTS 严格模式下有一个重要的约束:对象字面量必须有显式类型声明(arkts-no-untyped-obj-literals规则)。
正确做法:
// ✅ 正确:为每个对象变量声明类型constSCENE_1:SceneItem={/* ... */};constSCENE_2:SceneItem={/* ... */};// ...// ✅ 导出时也显式声明类型constALL_SCENES:SceneItem[]=[SCENE_1,SCENE_2,SCENE_3,SCENE_4,SCENE_5,SCENE_6,SCENE_7,SCENE_8];// ❌ 错误:数组字面量无法推断类型constALL_SCENES=[{id:1,name:'黎明破晓',...},// 编译报错!{id:2,name:'朝露晨光',...}];这就是为什么我们要先声明独立变量(SCENE_1: SceneItem),再组装成数组的原因。
二、分类体系设计
场景分为 5 个分类,应用首页和场景列表页都需要用到:
exportconstCATEGORIES:string[]=['全部','晨光','森林','海洋','夕阳','星夜'];提供三个查询函数:
// 获取所有场景exportfunctiongetScenes():SceneItem[]{returnALL_SCENES;}// 按 ID 查找单个场景exportfunctiongetSceneById(id:number):SceneItem|undefined{returnALL_SCENES.find(item=>item.id===id);}// 按分类筛选场景exportfunctiongetScenesByCategory(category:string):SceneItem[]{if(category==='全部'||category===''){returnALL_SCENES;}returnALL_SCENES.filter(item=>item.category===category);}这种设计简洁清晰,数据与 UI 完全解耦。如果需要后端数据,只需要将查询函数改为异步请求,页面代码无需改动。
三、@State 装饰器 —— 组件的状态驱动
ArkTS 中的@State是核心状态管理装饰器。当被@State修饰的变量发生变化时,UI 会自动重新渲染。
基本用法
@Componentstruct ScenePage{@Statescenes:SceneItem[]=getScenes();// 场景列表 → 变化时刷新网格@StateselectedCategory:string='全部';// 选中分类 → 变化时刷新筛选build(){Column(){// 分类标签点击 → 更新 selectedCategory → 触发 UI 重绘ForEach(CATEGORIES,(cat:string)=>{Text(cat).onClick(()=>{this.selectedCategory=cat;// 更新状态this.scenes=getScenesByCategory(cat);// 更新数据})})}}}@State 的更新机制
// 方式1:直接赋值(基本类型)@Statename:string='黎明破晓';this.name='星空璀璨';// ✅ UI 更新// 方式2:数组整体替换(推荐)@Statescenes:SceneItem[]=[];this.scenes=getScenesByCategory('海洋');// ✅ UI 更新// 方式3:数组方法(需要确保引用变化)// ⚠️ 如果只 push/splice 不重新赋值,UI 不会更新this.scenes.push(newItem);// ❌ UI 未必更新this.scenes=[...this.scenes,newItem];// ✅ 新数组触发更新@State 的生命周期
@State变量在组件创建时初始化,在组件销毁时释放。如果需要在页面出现时重新加载数据,使用aboutToAppear生命周期:
@Componentstruct FavPage{@StatefavScenes:SceneItem[]=[];@StatefavCount:number=0;// 页面出现前调用 —— 适合加载数据aboutToAppear():void{this.loadFavScenes();}}四、AppStorage —— 全局状态共享
当需要在不同页面之间共享数据时,AppStorage是最佳选择。它是应用级的键值存储,所有页面都可以读写。
初始化与使用
// 在首页初始化收藏列表aboutToAppear():void{if(!AppStorage.has(FAV_KEY)){AppStorage.set<number[]>(FAV_KEY,[]);}}// 在任意页面读取constfavList:number[]=AppStorage.get<number[]>(FAV_KEY)||[];// 写入更新AppStorage.set<number[]>(FAV_KEY,favList);收藏功能实战
我们定义FAV_KEY常量:
exportconstFAV_KEY:string='fav_scenes';收藏的数据流:
用户点击收藏 ❤️ ↓ toggleFav() → 更新 AppStorage ↓ DetailPage 显示收藏状态 ↓ FavPage 在 aboutToAppear 中读取 AppStorage → 展示收藏列表 ↓ ProfilePage 在 aboutToAppear 中读取 AppStorage → 展示收藏数量DetailPage 中的收藏切换逻辑:
// 检查是否已收藏checkFavStatus():void{constfavList:number[]=AppStorage.get<number[]>(FAV_KEY)||[];this.isFav=this.scene?favList.indexOf(this.scene.id)>=0:false;}// 切换收藏toggleFav():void{letfavList:number[]=AppStorage.get<number[]>(FAV_KEY)||[];constidx=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<number[]>(FAV_KEY,favList);}为什么选择 AppStorage 而不是本地文件?
| 方案 | 读写速度 | 跨页面 | 持久化 | 使用场景 |
|---|---|---|---|---|
| @State | 瞬间 | ❌ 仅当前组件 | ❌ | 组件内部状态 |
| AppStorage | 瞬间 | ✅ 所有页面 | ❌ 重启丢失 | 运行时全局状态 |
| Preferences | 毫秒级 | ✅ | ✅ | 用户配置、收藏持久化 |
| 数据库 | 取决于数据量 | ✅ | ✅ | 大量结构化数据 |
注意:AppStorage 存入的数组是引用,修改数组元素后必须重新
set()才能触发 UI 更新。
五、@Builder 装饰器 —— 组件化的利器
@Builder是 ArkTS 中定义可复用 UI 片段的语法,类似于其他框架中的函数组件。
基础用法
@BuilderSceneCard(item:SceneItem){Column(){Text(item.name).fontSize(18).fontColor(Color.White);Text(item.desc).fontSize(12).fontColor($r('app.color.text_secondary'));// ...}.borderRadius(16).onClick(()=>{router.pushUrl({url:'pages/DetailPage',params:{sceneId:item.id}});})}@Builder 的优势
- 代码复用:同一卡片在场景列表和收藏列表中可以复用
- 参数化:通过参数传递数据,灵活适配不同场景
- 链式调用:可以在 builder 中直接链式配置样式
@Builder 与自定义组件的选择
| 对比 | @Builder | 自定义 @Component |
|---|---|---|
| 状态管理 | 无独立状态 | 有独立 @State |
| 复用范围 | 只能在当前结构体中使用 | 可导出跨文件使用 |
| 性能 | 轻量,无额外开销 | 略微重一些 |
| 适用场景 | 简单 UI 片段、卡片 | 复杂交互、独立功能模块 |
六、数据流设计模式总结
整个应用的数据流设计如下:
SceneData.ets(数据仓库) ├── SceneItem 接口(类型定义) ├── 8 个场景对象(数据实例) ├── 3 个查询函数(数据访问层) └── FAV_KEY 常量(数据键名) │ ▼ AppStorage(全局状态层) │ ┌────┼────┬────┬────┐ ▼ ▼ ▼ ▼ ▼ Index ScenePage DetailPage FavPage ProfilePage (页面UI层,通过 @State 驱动)关键原则:
- 数据与 UI 分离:所有数据集中在 SceneData.ets,UI 页面只负责展示
- 单向数据流:数据从 Model → @State → UI,用户交互 → 回调 → 更新 Model
- 共享状态集中管理:跨页面数据用 AppStorage,避免参数层层传递
七、踩坑记录
坑1:数组更新不触发 UI 重绘
// ❌ 直接修改数组元素this.scenes[0].name='新名字';// UI 不变// ✅ 替换整个数组constnewScenes=[...this.scenes];newScenes[0]={...newScenes[0],name:'新名字'};this.scenes=newScenes;坑2:对象字面量编译报错
现象:编译错误arkts-no-untyped-obj-literals
解决:给每个对象变量显式声明类型,不要用类型推断
坑3:AppStorage 存数组后读取为空
原因:AppStorage key 未初始化时get()返回 undefined
解决:用|| []兜底,并在首页的aboutToAppear中初始化
总结
本篇我们学习了:
- ✅ 数据模型定义与 ArkTS 严格模式规范
- ✅ @State 装饰器驱动 UI 更新
- ✅ AppStorage 全局状态共享实现收藏功能
- ✅ @Builder 组件化 UI 复用
- ✅ 数据流设计模式与最佳实践
状态管理是 ArkTS 开发的核心技能,掌握好这些机制,后续开发就会非常顺畅。下一篇我们将进入 UI 层面,看看如何用 ArkTS 实现沉浸式的光影视觉效果。
下一篇预告:沉浸式 UI 设计与组件化开发 —— 渐变背景、毛玻璃效果、光影动画实战