适合谁看

  • 想写可维护鸿蒙卡片数据层的人

  • 正在做每日推荐、排行榜、轮播类鸿蒙卡片的人

  • 不想把数据硬写进鸿蒙 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 个字段。这让数据项结构先稳定下来。

字段设计的考量:

字段

类型

用途

为什么需要

id

string

点击跳转到菜品详情页

卡片点击需要传参数

name

string

卡片上显示菜名

核心展示信息

region

string

卡片上显示地区

帮助用户判断兴趣

imageResName

string

鸿蒙图片资源名

卡片需要显示图片

highlight

string

口味亮点标签

吸引用户点击

summary

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 会检查传入的资源名是否在白名单中,如果不在就返回兜底图片。

这个设计的好处:

  1. 防止资源不存在导致崩溃 — 鸿蒙卡片如果引用了不存在的 $r() 资源,会直接崩溃

  2. 新增菜品时只需加白名单 — 不需要改校验逻辑

  3. 兜底行为明确 — 用户永远看不到空白图片

五、职责 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 不影响数据

兜底完善

资源异常时不会崩溃

关键代码位置

文件

作用

app/ohos/entry/src/main/ets/formability/RecommendData.ets

鸿蒙卡片数据层(本文核心)

app/ohos/entry/src/main/ets/formability/DailyRecommendFormAbility.ets

消费数据层

数据层职责全景图

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 这种组织方式很适合"每日推荐"类卡片:

  1. 数据结构稳定RecommendItem 接口定义了 6 个字段

  2. 数据和兜底并存RECOMMEND_LIST + FALLBACK_ITEM

  3. 资源校验完善VALID_IMAGE_RES_NAMES 白名单 + resolveImageResName 兜底

  4. 选择算法轻量 — 日期轮询,不依赖网络

  5. 职责分离 — 数据层管内容,Ability 管生命周期

先把数据结构、兜底和轮换规则单独收好,后面维护会轻很多。这份代码很适合当鸿蒙卡片数据层组织的第一块样板。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐