【鸿蒙原生应用开发实战】第二篇:数据模型与状态管理 — 彻底搞懂 ArkTS 的数据驱动机制
前言
在上一篇中我们搭建了项目架构并完成了首页开发。这一篇我们将深入 ArkTS 的核心 —数据模型定义与状态管理机制。这是整个应用的"发动机":数据模型决定了App能展示什么,状态管理决定了UI如何响应数据变化。
本文你将学到:
- ArkTS 的
interface与class的正确使用姿势 - 静态工具类
FavoriteManager的设计模式 @State状态管理的完整工作流程- 数据流在页面间的传递方式
- 鸿蒙严格模式下的类型约束规则
一、数据模型层设计思路
在鸿蒙原生开发中,数据模型是业务逻辑的起点。我们遵循"数据驱动UI"的理念,所有的页面展示都由数据模型决定。
"宇宙探索"App 的数据架构
数据层(CelestialData.ets) ├── interface CelestialData ← 数据结构定义 ├── const CELESTIAL_LIST ← 静态数据源(10个天体) └── class FavoriteManager ← 收藏管理工具类 ↑ 业务层(各页面组件) ├── Index.ets ← 首页展示 ├── CelestialPage.ets ← 列表筛选 ├── DetailPage.ets ← 详情展示与收藏 ├── FavPage.ets ← 收藏管理 └── ProfilePage.ets ← 个人统计二、CelestialData 接口设计
2.1 接口定义
// entry/src/main/ets/model/CelestialData.etsexportinterfaceCelestialData{id:number;name:string;englishName:string;type:string;description:string;mass:string;diameter:string;distance:string;temperature:string;fact:string;color:string;isFavorite:boolean;}每个字段的设计考量:
| 字段 | 类型 | 用途 | 示例值 |
|---|---|---|---|
id | number | 唯一标识,用于路由传参和收藏管理 | 1, 2, 3… |
name | string | 天体中文名 | “太阳” |
englishName | string | 英文名,增加国际感 | “Sun” |
type | string | 分类标签 | “恒星”、“行星”、“星系” |
description | string | 详细描述文本 | “太阳是太阳系的中心天体…” |
mass | string | 质量信息 | “1.989 × 10³⁰ kg” |
diameter | string | 直径信息 | “1,392,700 km” |
distance | string | 距地距离 | “1.496 × 10⁸ km” |
temperature | string | 温度信息 | “表面 5,500°C” |
fact | string | 趣味知识 | “太阳每秒钟将约400万吨物质转化为能量” |
color | string | 主题色(十六进制) | “#FF6B35” |
isFavorite | boolean | 收藏状态标记 | false |
2.2 完整数据源
我们准备了10个天体数据,覆盖了5个分类:
exportconstCELESTIAL_LIST:CelestialData[]=[{id:1,name:'太阳',englishName:'Sun',type:'恒星',description:'太阳是太阳系的中心天体,占据了太阳系总质量的99.86%...',mass:'1.989 × 10³⁰ kg',diameter:'1,392,700 km',distance:'1.496 × 10⁸ km(1天文单位)',temperature:'表面 5,500°C / 核心 1,500万°C',fact:'太阳每秒钟将约400万吨物质转化为能量...',color:'#FF6B35',isFavorite:false},{id:2,name:'水星',/* ... */color:'#A0A0A0',isFavorite:false},{id:3,name:'金星',/* ... */color:'#E8C07A',isFavorite:false},{id:4,name:'地球',/* ... */color:'#4B7B8A',isFavorite:false},{id:5,name:'火星',/* ... */color:'#C1440E',isFavorite:false},{id:6,name:'木星',/* ... */color:'#D4A574',isFavorite:false},{id:7,name:'土星',/* ... */color:'#D4B896',isFavorite:false},{id:8,name:'银河系',type:'星系',color:'#6B8EC4',isFavorite:false},{id:9,name:'猎户座大星云',type:'星云',color:'#FF69B4',isFavorite:false},{id:10,name:'黑洞',type:'天文现象',color:'#2D2D3D',isFavorite:false}];数据覆盖了:恒星(1) + 行星(6) + 星系(1) + 星云(1) + 天文现象(1),足够支撑各个页面的分类筛选和展示。
类型推断注意:在 ArkTS 严格模式下,数组字面量必须能被推断出类型。这里
CELESTIAL_LIST显式标注为CelestialData[],字面量对象的结构也会被严格校验。
三、FavoriteManager 收藏管理器
收藏功能是整个App中最核心的交互,我们用一个静态工具类来管理收藏状态。
3.1 完整实现
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;}}3.2 设计模式分析
为什么用静态类而不是全局变量?
- 封装性:所有操作收藏的方法集中在一个类中,逻辑内聚
- 可维护性:未来如果需要持久化(比如用
PersistentStorage或数据库),只需修改FavoriteManager内部实现,不影响调用方 - 可测试性:静态方法易于单元测试
为什么不使用 @State + 全局状态管理?
这是一个篇幅适中的App,页面间数据传递并不复杂。用静态类比引入状态管理库(如 Redux / Pinia)更轻量,开发效率更高。
3.3 可扩展设计
如果你想让收藏数据持久化(退出App后不丢失),只需改造FavoriteManager:
// 持久化版本(示例,使用AppStorage)exportclassFavoriteManager{@StorageProp('favorites')staticfavorites:number[]=[];statictoggle(id:number):boolean{constindex=this.favorites.indexOf(id);if(index>=0){this.favorites.splice(index,1);AppStorage.set<number[]>('favorites',[...this.favorites]);returnfalse;}else{this.favorites.push(id);AppStorage.set<number[]>('favorites',[...this.favorites]);returntrue;}}// ...其他方法类似改造}四、@State 状态管理深入理解
4.1 @State 的本质
在 ArkTS 中,@State装饰的变量会被框架"监视"——当变量值变化时,框架自动重新渲染依赖于该变量的UI部分。
@Statecategories:CategoryItem[]=[/* ... */];@StatehotList:CelestialData[]=CELESTIAL_LIST.slice(0,5);@StaterecommendList:CelestialData[]=CELESTIAL_LIST.slice(5,10);关键规则:
@State变量变化 → 依赖该变量的UI自动刷新- 只有
@State变量变化才会触发刷新,普通变量不会 @State是组件级状态,不同组件实例的@State相互独立
4.2 数据驱动流程图
用户操作(点击收藏按钮) ↓ FavoriteManager.toggle(id) ← 修改数据层 ↓ @State isFav = ... ← 状态变量更新 ↓ 框架检测到 @State 变化 ↓ 自动重新渲染 build() ← UI刷新4.3 实战中的状态管理场景
场景一:列表页收藏状态
在CelestialCard组件中,每个卡片有自己的收藏状态:
struct CelestialCard{item:CelestialData={/* ... */};@StateisFav:boolean=false;aboutToAppear():void{this.isFav=FavoriteManager.isFavorite(this.item.id);}toggleFav():void{constnewState=FavoriteManager.toggle(this.item.id);this.isFav=newState;// 触发UI刷新this.item.isFavorite=newState;// 同步到数据对象}}当用户点击收藏按钮时:
toggleFav()被调用FavoriteManager.toggle()修改数据层this.isFav = newState触发@State刷新- 收藏图标从 ☆ 变为 ★(或相反)
场景二:详情页收藏
struct DetailPage{@Statedata:CelestialData={/* ... */};@StateisFav:boolean=false;aboutToAppear():void{// 从路由参数获取id,找到对应天体数据constparams=router.getParams()asRecord<string,Object>;if(params&¶ms['id']!==undefined){constid=Number(params['id']);for(leti=0;i<CELESTIAL_LIST.length;i++){if(CELESTIAL_LIST[i].id===id){this.data=CELESTIAL_LIST[i];break;}}}this.isFav=FavoriteManager.isFavorite(this.data.id);}toggleFav():void{this.isFav=FavoriteManager.toggle(this.data.id);this.data.isFavorite=this.isFav;}}场景三:收藏列表页
struct FavPage{@StatefavList:CelestialData[]=[];onPageShow():void{this.loadFavorites();// 每次页面显示时刷新}loadFavorites():void{constfavIds=FavoriteManager.getAll();constresult:CelestialData[]=[];for(leti=0;i<CELESTIAL_LIST.length;i++){if(favIds.indexOf(CELESTIAL_LIST[i].id)>=0){result.push(CELESTIAL_LIST[i]);}}this.favList=result;// 触发UI刷新}}注意
onPageShowvsaboutToAppear:
aboutToAppear— 仅在组件首次创建时调用一次onPageShow— 每次页面显示(包括从其他页面返回)时都调用收藏列表页需要在每次返回时刷新数据,所以用
onPageShow。
五、路由传参中的数据流
5.1 路由传递参数
// 从首页跳转到详情页router.pushUrl({url:'pages/DetailPage',params:{id:this.item.id}});// 从首页跳转到分类列表router.pushUrl({url:'pages/CelestialPage',params:{filterType:this.category.type}});5.2 目标页面接收参数
// DetailPage 中aboutToAppear():void{constparams=router.getParams()asRecord<string,Object>;if(params&¶ms['id']!==undefined){constid=Number(params['id']);// 根据id查找天体数据...}}// CelestialPage 中aboutToAppear():void{constparams=router.getParams()asRecord<string,Object>;if(params&¶ms['filterType']!==undefined){this.filterType=String(params['filterType']);this.activeTab=this.filterType;// 选中对应标签}this.applyFilter();}⚠️
router.getParams()返回类型为Record<string, Object>,需要用as断言后取值。取出的值可能需要转换为预期的类型(如Number()、String())。
5.3 整个数据流链路
Index(首页) │ 用户点击分类卡片 │ router.pushUrl({ url: 'pages/CelestialPage', params: { filterType: '行星' }}) ▼ CelestialPage(分类列表) │ aboutToAppear() 读取 params.filterType │ @State activeTab = '行星' → applyFilter() 筛选 → 渲染列表 │ 用户点击天体卡片 │ router.pushUrl({ url: 'pages/DetailPage', params: { id: 4 }}) ▼ DetailPage(详情页) │ aboutToAppear() 读取 params.id │ 遍历 CELESTIAL_LIST 找到 id=4 → @State data = 地球数据 │ 用户点击收藏 → FavoriteManager.toggle(4) → @State isFav 刷新六、ArkTS 严格模式注意事项
HarmonyOS 6.1 的 ArkTS 编译器在默认开启的严格模式下有一些关键约束,不遵守会导致编译失败。
6.1 对象字面量必须显式类型
// ❌ 错误 - 编译器无法推断字面量类型@Statecategories=[{name:'行星',type:'行星',icon:'🪐'}];// ✅ 正确 - 显式标注类型interfaceCategoryItem{name:string;type:string;icon:string;}@Statecategories:CategoryItem[]=[{name:'行星',type:'行星',icon:'🪐'}];6.2 组件属性的默认值
@Componentstruct CelestialCard{// ❌ 错误 - 属性必须有默认值,且类型必须标注item:CelestialData;// ✅ 正确 - 提供完整的默认对象item:CelestialData={id:0,name:'',englishName:'',type:'',description:'',mass:'',diameter:'',distance:'',temperature:'',fact:'',color:'#FFFFFF',isFavorite:false};}6.3 数组字面量推断
// ❌ 错误 - 编译器无法推断字面量数组的类型constlist=[{id:1,name:'test'}];// ✅ 正确 - 显式标注数组类型constlist:DataItem[]=[{id:1,name:'test'}];6.4 ForEach 的 key 函数
在 ArkTS 中,ForEach支持三个参数,第三个是key 生成函数,用于优化列表 diff:
ForEach(this.infoItems,(item:InfoPair)=>InfoItem({label:item.label,value:item.value}),(item:InfoPair)=>item.label// key 生成函数)如果列表中没有重复项,也可以省略第三个参数,但当列表项可能变化时提供 key 可以提升渲染性能。
七、本篇总结
本篇我们深入探讨了:
- ✅数据模型设计—
CelestialData接口和CELESTIAL_LIST常量数据 - ✅FavoriteManager 工具类— 封装收藏逻辑,面向未来可扩展
- ✅@State 状态管理— 理解数据驱动UI的核心机制
- ✅路由传参数据流— 页面间的参数传递与接收
- ✅严格模式约束— 对象字面量、组件属性、数组类型的正确写法
核心思考:鸿蒙 ArkTS 采用"数据驱动UI"范式,我们不需要手动操作DOM或调用 setState(),只需要修改
@State变量的值,框架自动处理UI刷新。这让代码更简洁、更可预测。
下篇预告:我们将开发最重要的页面之一 — CelestialPage(天体列表页)。你将学到标签筛选的实现原理、ForEach 的多种渲染技巧,以及 CCard 组件的完整设计。
本篇涉及的文件:
entry/src/main/ets/model/CelestialData.ets— 数据模型- 所有页面文件都会引用这个模型