鸿蒙Next实战-笑话大全App开发
鸿蒙 Next 实战:从零开发「笑话大全」App — 本地持久化 + 下拉刷新 + 上拉加载更多
目标 API 24+(HarmonyOS 7.0+) | 开发工具:DevEco Studio 6.1 | 语言:ArkTS
一、前言
在移动应用开发中,内容型应用(段子、资讯、短视频 Feed)是最常见的形态之一。这类应用的核心操作无非三件事:首次加载 → 下拉刷新 → 上拉加载更多。听起来简单,但要做得优雅——骨架屏占位、加载状态管理、分页边界处理、离线缓存——每个环节都有不少细节。
本文将以一个完整的 「笑话大全」App 为例,带你走一遍 HarmonyOS Next 上这整套流程的工程化实现。全部代码使用 ArkTS 编写,目标 API 24+(HarmonyOS 7.0+),兼顾可读性与性能。
二、项目概览
2.1 功能清单
| 功能 | 实现方式 |
|---|---|
| 笑话列表展示 | List + ForEach,渲染 30 条带分类标签的卡片 |
| 下拉刷新 | Refresh 组件 + pullToRefresh 属性 |
| 上拉加载更多 | onScrollIndex 检测 + 分页追加 |
| 本地持久化 | preferences(@kit.ArkData),JSON 序列化 |
| 种子数据 | 内置 30 条中文段子,按 5 种分类轮转 |
| 骨架屏 | 首次加载时显示占位卡片 |
| 加载指示器 | List 底部的 LoadingProgress + 文字提示 |
| 已到底提示 | 分隔线 + “已经到底了 😄” |
2.2 目录结构
entry/src/main/ets/
├── models/
│ └── Joke.ets # Joke 接口定义
├── repository/
│ └── JokeRepository.ets # 数据仓库:种子数据 + preferences 读写 + 分页
└── pages/
└── Index.ets # 主页面,含 6 个自定义组件
三、架构设计思路
3.1 分层
我们把代码分成三层:
┌─────────────────────┐
│ UI 层 (Pages) │ ← 组件状态、布局、滚动事件
├─────────────────────┤
│ Repository 层 │ ← 数据读写、分页逻辑、序列化
├─────────────────────┤
│ Model 层 │ ← 纯数据类型定义
└─────────────────────┘
- Model 层:只定义
Joke接口,没有任何行为。 - Repository 层:封装所有数据操作的细节。UI 层不直接碰
preferences,也不关心 JSON 序列化。 - UI 层:只关心"给我数据"和"告诉我有没有下一页",具体的分页计算由 Repository 完成。
这种分层的好处是:如果你想日后把本地存储换成远程 API,只需要改 Repository 内部实现,UI 层几乎不用动。
3.2 数据流
用户下拉 ──→ Refresh.onRefreshing ──→ repository.init() ──→ getFirstPage()
│
▼
用户上滑 ──→ List.onScrollIndex ──→ repository.getNextPage(n) ──→ displayedJokes 更新
│
▼
preferences.put(JSON.stringify(allJokes)) ← 每次写入持久化
四、Model 层:Joke 数据模型
models/Joke.ets
export interface Joke {
id: string;
content: string;
category: string;
createdAt: string;
}
ArkTS 的 interface 编译时会被擦除,不影响包体积。这里用 string 类型的 createdAt 而非 Date 对象,是因为 JSON.stringify/parse 天然支持字符串,无需自定义 reviver。
五、Repository 层:本地持久化 + 分页
5.1 种子数据
内置 30 条中文段子,覆盖 5 个分类:
const categories: string[] = [
'冷笑话', '程序员笑话', '生活段子', '职场趣事', '夫妻日常'
];
每条笑话通过 index % 5 轮转分配分类,保证列表天然有视觉节奏感。
5.2 preferences 读写
HarmonyOS 的 @kit.ArkData 提供了 preferences 轻量级 KV 存储。使用姿势:
import { preferences } from '@kit.ArkData';
// 打开/创建存储实例
this.preferences_ = await preferences.getPreferences(context, 'joke_app_db');
// 写入
await this.preferences_.put('my_key', JSON.stringify(data));
await this.preferences_.flush(); // 立即落盘
// 读取
const value = await this.preferences_.get('my_key', '');
const parsed = JSON.parse(value as string);
⚠️ 关键细节:
get()返回的是Promise<ValueType>,必须await。API 23+ 的类型声明中get的第二个参数是默认值,类型为ValueType,需要as string窄化。
5.3 分页逻辑
Repository 提供两个核心方法:
/** 第一页(前 PAGE_SIZE 条) */
getFirstPage(): Joke[] {
return this.allJokes_.slice(0, this.pageSize_);
}
/** 从完整列表中取到 currentCount + PAGE_SIZE 条 */
getNextPage(currentCount: number): Joke[] {
const end = Math.min(currentCount + this.pageSize_, this.allJokes_.length);
return this.allJokes_.slice(0, end);
}
/** 是否还有更多 */
hasMore(currentCount: number): boolean {
return currentCount < this.allJokes_.length;
}
这里的设计要点是:用 slice(0, end) 而非维护一个 currentPage 游标。这样做的好处是:
- 如果用户在加载过程中新增了一条笑话(插入到数组头部),
slice(0, end)天然会把新数据包含进来,不会出现"已加载列表"与"底层数据"不一致。 - 逻辑简单,零状态维护。
5.4 ArkTS 对象字面量约束
ArkTS 对类型安全要求严格,禁止"匿名对象字面量"。你必须显式声明类型:
// ❌ ArkTS 编译错误:Object literal must correspond to some explicitly declared class or interface
return {
id: 'xxx',
content: '...',
category: '...',
createdAt: '...',
};
// ✅ 正确写法:先声明类型
const joke: Joke = {
id: 'xxx',
content: '...',
category: '...',
createdAt: '...',
};
return joke;
这是 ArkTS 与普通 TypeScript 的重要区别之一,初接触时容易踩坑。
六、UI 层:列表 + 刷新 + 加载更多
6.1 整体结构
Stack
└─ Column
├─ TitleBar (橙色标题栏)
└─ Refresh
└─ List
├─ JokeCard × N
├─ LoadingFooter / EndFooter
6.2 Refresh 组件
HarmonyOS 的 Refresh 组件用法简洁:
Refresh({ refreshing: $$this.isRefreshing }) {
// 列表内容
}
.onRefreshing(() => {
this.onRefreshAction();
})
.pullToRefresh(true)
refreshing绑定双向状态$$this.isRefreshingonRefreshing回调中执行刷新逻辑pullToRefresh(true)启用下拉手势
⚠️ 注意:API 23 中回调名为
onRefreshing,不是onRefresh。两者区别在于onRefreshing是 Refresh 组件专属的事件回调,而onRefresh不存在于 RefreshAttribute 上。
6.3 上拉加载更多:onScrollIndex
List 组件提供了 onScrollIndex 回调,会在滚动时向应用报告当前可见的第一项和最后一项的索引:
List() { ... }
.onScrollIndex((firstIndex: number, lastIndex: number) => {
const threshold = 3;
if (lastIndex >= this.displayedJokes.length - threshold
&& this.hasMoreData && !this.isLoading) {
this.loadMore();
}
})
当最后可见索引 lastIndex 距离列表末尾不足 3 项时,触发加载更多。threshold = 3 是为了预加载——在用户划到最底部之前就开始加载下一页,消除等待感。
6.4 卡片组件(JokeCard)
每个笑话卡片的结构:
┌─────────────────────────────────┐
│ ❄️ 冷笑话 3天前 │
│ │
│ 为什么程序员总是把万圣节和... │
│ 因为 Oct 31 和 Dec 25 在... │
└─────────────────────────────────┘
卡片采用全圆角(12vp) + 浅阴影设计,分类标签使用品牌色 #FF6B35 的浅色背景,时间文本使用灰色弱化视觉权重。
6.5 组件属性可见性
ArkTS 中 @Component struct 的属性,如果通过构造函数传值,不能标记为 private:
// ❌ 编译错误:Property 'showBack' is private and can not be initialized through constructor
@Component
struct TitleBar {
private showBack: boolean = false; // 不可用
}
// ✅ 正确
@Component
struct TitleBar {
showBack: boolean = false; // public,允许构造器传值
}
这是因为 ArkTS 的构造函数语法 TitleBar({ showBack: true }) 本质上是在初始化 struct 的字段,而 private 阻止了外部访问。
七、关于 API 24
7.1 为什么选择 API 24+
HarmonyOS API 24(对应 HarmonyOS 7.0+)相比 API 23 引入了更完善的 ArkUI 组件能力和更严格的类型检查。本文的代码在 API 23 上也可编译运行,但为了面向未来,项目配置已预留 API 24 兼容:
// build-profile.json5
{
"app": {
"products": [{
"targetSdkVersion": "7.0.0(24)",
"compatibleSdkVersion": "7.0.0(24)",
}]
}
}
7.2 迁移检查清单
从 API 23 → API 24 迁移时,需要关注:
- SDK 安装:通过 DevEco Studio → Settings → SDK Manager 下载 HarmonyOS 7.0+ SDK
- 模块依赖:检查
oh-package.json5中依赖的 kit 版本是否支持 API 24 - 废弃 API:本文代码中的
getContext(this)在 API 23 已标记废弃,API 24 可改用this.getUIContext() - 代码无需改动:本文所有组件(Refresh、List、LoadingProgress、preferences)均在 API 8+ 就存在,API 24 无破坏性变更
八、完整代码解析
8.1 Joke.ets — 13 行
纯粹的接口定义,零依赖。
8.2 JokeRepository.ets — 212 行
将上面讲到的所有逻辑整合在一起:
init() → 获取 preferences 实例 → 加载或创建种子数据
loadFromStorage() → await get() → JSON.parse
saveToStorage() → put() → flush()
getFirstPage() → slice(0, PAGE_SIZE)
getNextPage(n) → slice(0, min(n+PAGE_SIZE, total))
hasMore(n) → n < total
createSeedJokes() → 30 条 → Joke[](显式类型)
addJoke() → unshift + save
resetToSeed() → 重新生成 + save
8.3 Index.ets — 416 行
包含 6 个 @Component:
| 组件 | 行数 | 职责 |
|---|---|---|
Index |
150+ | 根组件,管理状态 + 事件 |
TitleBar |
36 | 橙色标题栏 |
JokeCard |
63 | 单条笑话卡片 |
LoadingFooter |
24 | “上拉加载更多”/“加载中…” |
EndFooter |
26 | “已经到底了 😄” |
LoadingSkeleton |
33 | 首次加载占位 |
LoadingDialog |
24 | 全屏加载弹窗 |
8.4 状态管理
Index 组件使用 5 个 @State:
@State displayedJokes: Joke[] = []; // 当前渲染的笑话列表
@State isLoading: boolean = false; // 是否正在加载
@State hasMoreData: boolean = true; // 是否还有下一页
@State isRefreshing: boolean = false; // 是否正在下拉刷新
@State isInitialized: boolean = false; // 首次加载是否完成
每个状态变化都会触发 ArkUI 的响应式重渲染,无需手动操作 DOM。
九、工程化注意事项
9.1 避免循环依赖
ArkTS 编译时要求 import 图无环。本文的分层方案天然避免了这一点:
Joke.ets ← JokeRepository.ets ← Index.ets
数据从 Repository 单向流向 UI。
9.2 ForEach 的 key 生成
ForEach 需要一个稳定且唯一的 key 生成器,用于列表 diff 优化:
ForEach(this.displayedJokes, (item: Joke) => {
ListItem() { JokeCard({ joke: item }) }
}, (item: Joke) => item.id) // ← 第三个参数:key 生成器
如果不用 key 生成器,ArkUI 在列表追加/删除时会全量重建,影响性能。
9.3 延迟模拟
为了让用户感知到加载过程(而非瞬间完成),loadMore() 中加入了 300ms 延迟:
async loadMore(): Promise<void> {
this.isLoading = true;
await this.delay(300);
// ... 加载数据
}
在实际应用中,如果使用远程 API,这个延迟自然会被网络请求取代,不需要人工加 delay。
十、总结
在本文中,我们完成了一个完整的 HarmonyOS Next 内容型 App:
- ✅ 本地持久化:preferences KV 存储,JSON 序列化,启动秒加载
- ✅ 下拉刷新:Refresh 组件,绑定
$$双向状态 - ✅ 上拉加载更多:onScrollIndex 预加载触发,slice 分页
- ✅ API 24+ 就绪:代码向前兼容,配置一键切换
- ✅ 优雅体验:骨架屏、分类标签、加载指示器、已到底提示
本文的全部代码可以在 [GitHub / Gitee] 获取。如果你正在从 Android/iOS 转向 HarmonyOS,或者从 TypeScript 转向 ArkTS,希望这篇文章对你有所帮助。
如果你有任何问题或改进建议,欢迎在评论区留言讨论!



更多推荐




所有评论(0)