鸿蒙 RecommendData 这类卡片数据层应该怎么组织
适合谁看
-
想写可维护鸿蒙卡片数据层的人
-
正在做每日推荐、排行榜、轮播类鸿蒙卡片的人
-
不想把数据硬写进鸿蒙 Ability 文件的人
问题背景
鸿蒙卡片代码很容易越写越乱的一个原因是:
-
Ability 管生命周期
-
Ability 还管数据
-
Ability 还管兜底
-
Ability 还管资源校验
最后所有逻辑都挤在同一个文件里,改一个数据要动 Ability,改一个兜底要动 Ability,改一个资源名也要动 Ability。
项目中的真实场景
食界探味当前把鸿蒙卡片数据层单独放在:
-
app/ohos/entry/src/main/ets/formability/RecommendData.ets
对应的 Ability 只负责消费它:
// DailyRecommendFormAbility.ets
import { getRecommendOfToday, resolveImageResName } from './RecommendData';
onAddForm(want: Want): formBindingData.FormBindingData {
const item = getRecommendOfToday();
return formBindingData.createFormBindingData({
dishName: item.name,
dishImage: resolveImageResName(item.imageResName),
// ...
});
}
Ability 不关心数据从哪来、怎么选、怎么校验,只关心"拿数据 → 绑定到卡片"。
核心实现
一、RecommendData.ets 的完整结构
// 1. 数据结构定义
export interface RecommendItem {
id: string; // 菜品 ID(用于点击跳转)
name: string; // 菜名
region: string; // 地区
imageResName: string; // 鸿蒙图片资源名
highlight: string; // 口味亮点
summary: string; // 一句话介绍
}
// 2. 推荐列表(10 道菜品)
const RECOMMEND_LIST: RecommendItem[] = [
{ id: 'beef-curry', name: '牛肉咖喱', region: '印度 · 亚洲', imageResName: 'dish_beef_curry', highlight: '浓郁香料', summary: '椰香与香料层层叠起,入口热烈又厚实。' },
{ id: 'sukiyaki', name: '寿喜锅', region: '日本 · 亚洲', imageResName: 'dish_sukiyaki', highlight: '鲜甜酱香', summary: '牛肉、蔬菜与寿喜烧汁一起慢慢煮到刚好。' },
// ... 共 10 道菜品
];
// 3. 兜底数据
const FALLBACK_ITEM: RecommendItem = {
id: 'fallback', name: '环球美食', region: '世界',
imageResName: 'dish_fallback', highlight: '今天吃什么',
summary: '打开食界探味,挑一道想去认识的新菜。',
};
// 4. 图片资源白名单
const VALID_IMAGE_RES_NAMES: Set<string> = new Set(
RECOMMEND_LIST.map((item) => item.imageResName).concat(FALLBACK_ITEM.imageResName)
);
// 5. 今日选择算法
export function getRecommendOfToday(): RecommendItem { ... }
// 6. 兜底获取
export function getFallbackItem(): RecommendItem { ... }
// 7. 图片资源校验
export function resolveImageResName(imageResName: string): string { ... }
这个文件承担了 7 类职责,每一类都值得单独分析。
二、职责 1:定义卡片数据结构
export interface RecommendItem {
id: string;
name: string;
region: string;
imageResName: string;
highlight: string;
summary: string;
}
通过 RecommendItem 明确了卡片需要的 6 个字段。这让数据项结构先稳定下来。
字段设计的考量:
|
字段 |
类型 |
用途 |
为什么需要 |
|---|---|---|---|
|
|
string |
点击跳转到菜品详情页 |
卡片点击需要传参数 |
|
|
string |
卡片上显示菜名 |
核心展示信息 |
|
|
string |
卡片上显示地区 |
帮助用户判断兴趣 |
|
|
string |
鸿蒙图片资源名 |
卡片需要显示图片 |
|
|
string |
口味亮点标签 |
吸引用户点击 |
|
|
string |
一句话介绍 |
补充信息,帮助决策 |
这个结构既不过于复杂(6 个字段),也不过于简单(包含了点击跳转需要的 id)。
三、职责 2:维护推荐列表和兜底项
const RECOMMEND_LIST: RecommendItem[] = [
// 10 道菜品...
];
const FALLBACK_ITEM: RecommendItem = {
id: 'fallback', name: '环球美食', region: '世界',
imageResName: 'dish_fallback', highlight: '今天吃什么',
summary: '打开食界探味,挑一道想去认识的新菜。',
};
RECOMMEND_LIST 是正常推荐数据,FALLBACK_ITEM 是兜底数据。
兜底项的价值:
|
场景 |
没有兜底 |
有兜底 |
|---|---|---|
|
列表为空 |
卡片显示空白 |
显示"环球美食" |
|
图片资源不存在 |
卡片渲染崩溃 |
显示兜底图片 |
|
数据异常 |
用户看到异常 |
用户看到正常兜底 |
鸿蒙卡片比应用页面更怕显示异常——因为卡片在桌面上,异常会一直显示,用户无法刷新。
四、职责 3:图片资源校验
const VALID_IMAGE_RES_NAMES: Set<string> = new Set(
RECOMMEND_LIST.map((item) => item.imageResName).concat(FALLBACK_ITEM.imageResName)
);
export function resolveImageResName(imageResName: string): string {
if (!imageResName || !VALID_IMAGE_RES_NAMES.has(imageResName)) {
return FALLBACK_ITEM.imageResName;
}
return imageResName;
}
VALID_IMAGE_RES_NAMES 是鸿蒙图片资源的白名单。resolveImageResName 会检查传入的资源名是否在白名单中,如果不在就返回兜底图片。
这个设计的好处:
-
防止资源不存在导致崩溃 — 鸿蒙卡片如果引用了不存在的
$r()资源,会直接崩溃 -
新增菜品时只需加白名单 — 不需要改校验逻辑
-
兜底行为明确 — 用户永远看不到空白图片
五、职责 4:今日选择算法
export function getRecommendOfToday(): RecommendItem {
if (RECOMMEND_LIST.length === 0) return FALLBACK_ITEM;
const now = new Date();
const dateNum = now.getFullYear() * 10000 + (now.getMonth() + 1) * 100 + now.getDate();
const index = dateNum % RECOMMEND_LIST.length;
return RECOMMEND_LIST[index];
}
日期轮询算法:用 年月日 生成数字,对列表长度取模。
|
日期 |
dateNum |
index (mod 10) |
菜品 |
|---|---|---|---|
|
2025-01-15 |
20250115 |
5 |
牛肉塔可 |
|
2025-01-16 |
20250116 |
6 |
石锅拌饭 |
|
2025-01-17 |
20250117 |
7 |
班尼迪克蛋 |
这个算法的特点:
-
同一天内结果一致 — 所有卡片展示同一道菜
-
不同天自动切换 — 不需要手动更新
-
列表为空时兜底 — 返回
FALLBACK_ITEM -
不依赖网络 — 纯本地计算
六、职责 5:兜底获取
export function getFallbackItem(): RecommendItem {
return FALLBACK_ITEM;
}
单独导出兜底项,方便其他地方使用(比如卡片 UI 的默认值)。
七、数据层和 Ability 的分工
RecommendData.ets(数据层)
│
├─ RecommendItem ← 数据结构
├─ RECOMMEND_LIST[] ← 推荐列表
├─ FALLBACK_ITEM ← 兜底数据
├─ VALID_IMAGE_RES_NAMES ← 资源白名单
├─ getRecommendOfToday() ← 选择算法
├─ getFallbackItem() ← 兜底获取
└─ resolveImageResName() ← 资源校验
DailyRecommendFormAbility.ets(Ability 层)
│
├─ onAddForm() ← 消费数据层
├─ onUpdateForm() ← 消费数据层
└─ onRemoveForm() ← 清理资源
Ability 只关心"拿数据 → 绑定到卡片",不关心数据从哪来、怎么选、怎么校验。
八、为什么这种组织方式值得复用
|
好处 |
说明 |
|---|---|
|
职责清晰 |
数据层管内容,Ability 管生命周期 |
|
易于测试 |
数据层可以独立测试,不需要启动 Ability |
|
易于扩展 |
新增菜品只需加 RECOMMEND_LIST |
|
易于维护 |
改数据不影响 Ability,改 Ability 不影响数据 |
|
兜底完善 |
资源异常时不会崩溃 |
关键代码位置
|
文件 |
作用 |
|---|---|
|
|
鸿蒙卡片数据层(本文核心) |
|
|
消费数据层 |
数据层职责全景图
RecommendData.ets
│
├─ 接口定义
│ └─ RecommendItem { id, name, region, imageResName, highlight, summary }
│
├─ 数据存储
│ ├─ RECOMMEND_LIST[] ← 10 道菜品
│ ├─ FALLBACK_ITEM ← 兜底数据
│ └─ VALID_IMAGE_RES_NAMES ← 图片资源白名单
│
├─ 选择算法
│ └─ getRecommendOfToday() ← 日期轮询
│
├─ 兜底获取
│ └─ getFallbackItem() ← 返回 FALLBACK_ITEM
│
└─ 资源校验
└─ resolveImageResName() ← 白名单校验 + 兜底
常见坑
-
把推荐列表直接写在 Ability 里 — 改数据要动 Ability,职责混乱
-
没有兜底数据项 — 鸿蒙卡片异常时显示空白
-
图片资源名不做校验 — 鸿蒙
$r()引用不存在的资源会崩溃 -
每次更新时间策略都散落在多个地方 — 应该集中管理
-
数据结构不稳定 — 新增字段时要改多个文件
-
没有导出数据结构 — 其他文件无法复用
RecommendItem
可复用模板
鸿蒙卡片数据层模板
// 1. 数据结构
export interface CardItem {
id: string;
title: string;
subtitle: string;
imageRes: string;
highlight: string;
summary: string;
}
// 2. 数据列表
const ITEM_LIST: CardItem[] = [
{ id: '1', title: '标题1', subtitle: '副标题1', imageRes: 'img_1', highlight: '亮点1', summary: '简介1' },
// ...
];
// 3. 兜底数据
const FALLBACK: CardItem = {
id: 'fallback', title: '默认标题', subtitle: '默认副标题',
imageRes: 'img_fallback', highlight: '默认亮点', summary: '默认简介',
};
// 4. 资源白名单
const VALID_RES: Set<string> = new Set(ITEM_LIST.map(i => i.imageRes).concat(FALLBACK.imageRes));
// 5. 选择算法
export function getTodayItem(): CardItem {
if (ITEM_LIST.length === 0) return FALLBACK;
const now = new Date();
const dateNum = now.getFullYear() * 10000 + (now.getMonth() + 1) * 100 + now.getDate();
return ITEM_LIST[dateNum % ITEM_LIST.length];
}
// 6. 资源校验
export function safeImage(name: string): string {
if (!name || !VALID_RES.has(name)) return FALLBACK.imageRes;
return name;
}
数据层职责清单
鸿蒙卡片数据层应该包含:
□ 数据结构定义(export interface)
□ 数据列表(const LIST)
□ 兜底数据(const FALLBACK)
□ 资源白名单(const VALID_RES)
□ 选择算法(getTodayItem)
□ 资源校验(safeImage)
□ 兜底获取(getFallback)
本篇总结
鸿蒙卡片数据层应该独立于 Ability 文件。RecommendData.ets 这种组织方式很适合"每日推荐"类卡片:
-
数据结构稳定 —
RecommendItem接口定义了 6 个字段 -
数据和兜底并存 —
RECOMMEND_LIST+FALLBACK_ITEM -
资源校验完善 —
VALID_IMAGE_RES_NAMES白名单 +resolveImageResName兜底 -
选择算法轻量 — 日期轮询,不依赖网络
-
职责分离 — 数据层管内容,Ability 管生命周期
先把数据结构、兜底和轮换规则单独收好,后面维护会轻很多。这份代码很适合当鸿蒙卡片数据层组织的第一块样板。
更多推荐


所有评论(0)