鸿蒙原生应用实战(四):收藏页面与底部导航实现——状态管理与跨页面交互
摘要: 本文详细介绍了鸿蒙原生应用收藏页面的实现过程,涵盖页面布局、数据结构设计、状态管理及交互逻辑。通过@State和@Watch实现分类筛选功能,支持按朝代(唐诗、宋词等)分组展示收藏内容。页面包含动态分类标签、可编辑的收藏卡片(含用户笔记和收藏日期),并处理了空状态场景。设计上采用ArkTS状态驱动UI,结合条件渲染和样式切换,实现流畅的编辑模式与分类切换交互,最终整合到底部导航栏中,完成应
鸿蒙原生应用实战(四):收藏页面与底部导航实现——状态管理与跨页面交互
前言
前三章我们完成了首页、诗词库、详情页和作者天地四个页面的开发。本章将完成最后一个核心页面——收藏页面,并实现全局底部导航栏的整合。
收藏页面的实现涉及了数据筛选、编辑模式、分类分组等复杂交互,是检验 ArkTS 状态管理能力的试金石。
一、收藏页面(CollectionPage.ets)
1.1 页面布局总览
┌──────────────────────────┐
│ 我的收藏 编辑 返回 │ ← 顶部栏
├──────────────────────────┤
│ ⭐ 6首 │ ← 收藏统计
│ 共收藏诗词 │
├──────────────────────────┤
│ 收藏分类 │
│ 📚全部 📜唐诗 🌸宋词 │ ← 横向分类标签
│ 🎭元曲 📗诗经 🌙五代词 │
├──────────────────────────┤
│ 📖 静夜思 五言绝句 │
│ 唐 · 李白 │ ← 收藏卡片
│ "思乡名篇,百读不厌" │
│ 收藏于 2025-06-15 │
├──────────────────────────┤
│ 📖 水调歌头 词 │
│ 宋 · 苏轼 │
│ "中秋绝唱,意境超然" │
│ 收藏于 2025-06-14 │
├──────────────────────────┤
│ ...(共 6 条) │
├──────────────────────────┤
│ 🏠首页 📚诗词库 👤作者 ⭐收藏 │
└──────────────────────────┘
1.2 数据结构
收藏数据包含诗词基本信息、收藏日期和用户笔记:
interface CollectionItem {
id: number;
title: string;
author: string;
dynasty: string;
type: string;
dateAdded: string; // 收藏日期
notes: string; // 用户笔记
}
// 收藏分类分组
interface CollectionGroup {
name: string;
icon: string;
count: number;
}
1.3 收藏分类分组
我们将收藏的诗词按朝代分类,每个分类显示对应的 emoji 和计数:
const groups: CollectionGroup[] = [
{ name: '唐诗', icon: '📜', count: 1 },
{ name: '宋词', icon: '🌸', count: 3 },
{ name: '元曲', icon: '🎭', count: 1 },
{ name: '诗经', icon: '📗', count: 1 },
{ name: '五代词', icon: '🌙', count: 1 }
];
1.4 数据过滤实现
收藏页面的数据过滤同样使用 @State + @Watch 模式:
@State @Watch('onGroupChange') selectedGroup: string = '';
@State editMode: boolean = false;
@State filteredList: CollectionItem[] = myCollections;
onGroupChange(): void {
if (this.selectedGroup === '') {
this.filteredList = myCollections;
return;
}
const dynastyMap: Record<string, string> = {
'唐诗': '唐', '宋词': '宋', '元曲': '元',
'诗经': '先秦', '五代词': '五代'
};
const targetDynasty: string = dynastyMap[this.selectedGroup] || '';
this.filteredList = myCollections.filter(
(c: CollectionItem) => c.dynasty === targetDynasty
);
}
1.5 分类标签高亮
分类标签使用 @State 驱动的条件样式:
// "全部"标签
Column() {
Text('📚').fontSize(28)
Text('全部').fontSize(11)
.fontColor(this.selectedGroup === '' ?
$r('app.color.accent_purple') : $r('app.color.text_primary'))
.fontWeight(this.selectedGroup === '' ?
FontWeight.Bold : FontWeight.Normal)
Text(myCollections.length.toString()).fontSize(10)
.fontColor($r('app.color.text_secondary'))
}
.width(60).alignItems(HorizontalAlign.Center)
.onClick(() => { this.selectedGroup = ''; })
.backgroundColor(this.selectedGroup === '' ?
$r('app.color.accent_purple') + '10' : Color.Transparent)
.borderRadius(12)
// 各分类标签
ForEach(groups, (g: CollectionGroup) => {
Column() {
Text(g.icon).fontSize(28)
Text(g.name).fontSize(11)
.fontColor(this.selectedGroup === g.name ?
$r('app.color.accent_purple') : $r('app.color.text_primary'))
Text(g.count.toString() + '首').fontSize(10)
.fontColor($r('app.color.text_secondary'))
}
.onClick(() => { this.selectedGroup = g.name; })
.backgroundColor(this.selectedGroup === g.name ?
$r('app.color.accent_purple') + '10' : Color.Transparent)
}, (g: CollectionGroup) => g.name)
1.6 收藏卡片设计
每条收藏记录展示完整信息,包含用户笔记(斜体显示):
@Builder
createCollectionCard(item: CollectionItem) {
Row() {
// 编辑模式下的选中框
if (this.editMode) {
Circle()
.width(22).height(22)
.stroke($r('app.color.accent_purple')).strokeWidth(2)
.fill(Color.Transparent).margin({ right: 10 })
}
Column() { Text('📖').fontSize(28) }
Column() {
// 标题 + 类型标签
Row() {
Text(item.title).fontSize(17).fontWeight(FontWeight.Bold)
Text(item.type).fontSize(10)
.fontColor($r('app.color.accent_purple'))
.padding({ left: 6, right: 6, top: 2, bottom: 2 })
.backgroundColor($r('app.color.accent_purple') + '15')
.borderRadius(4).margin({ left: 8 })
}
Text(item.dynasty + ' · ' + item.author).fontSize(12)
.fontColor($r('app.color.text_secondary'))
// 用户笔记(斜体显示)
Text(item.notes).fontSize(13)
.fontColor($r('app.color.text_primary'))
.fontStyle(FontStyle.Italic)
.maxLines(1).textOverflow({ overflow: TextOverflow.Ellipsis })
Text('收藏于 ' + item.dateAdded).fontSize(11)
.fontColor($r('app.color.text_secondary'))
}
.layoutWeight(1).padding({ left: 12 })
if (!this.editMode) {
Text('>').fontSize(18).fontColor($r('app.color.text_secondary'))
}
}
.width('100%').padding(14)
.backgroundColor($r('app.color.bg_card')).borderRadius(12)
.onClick(() => {
if (!this.editMode) {
router.pushUrl({
url: 'pages/PoemDetailPage',
params: { poemId: item.id }
});
}
})
}
1.7 空状态处理
当筛选结果为空时,展示友好的空状态提示:
if (this.filteredList.length === 0) {
Column() {
Text('📭').fontSize(48)
Text('暂无收藏').fontSize(16)
.fontColor($r('app.color.text_secondary'))
.margin({ top: 12 })
}
.width('100%').height(200)
.alignItems(HorizontalAlign.Center)
.justifyContent(FlexAlign.Center)
}
二、编辑模式实现
收藏页面支持编辑模式,点击顶部"编辑"按钮切换:
Row() {
Text('我的收藏').fontSize(24).fontWeight(FontWeight.Bold)
Blank()
Text(this.editMode ? '完成' : '编辑')
.fontSize(14).fontColor($r('app.color.accent_purple'))
.onClick(() => { this.editMode = !this.editMode; })
}
编辑模式下,每个卡片左侧显示圆形选中框,方便用户批量操作。
三、底部导航栏
3.1 统一设计
所有 5 个页面底部共享同一套导航栏,包含 4 个 Tab:首页、诗词库、作者、收藏。
导航栏放在页面的最底层,使用 shadow 属性创建阴影效果:
Row() {
this.navItem('🏠', '首页', 'home', activePage, 'pages/Index')
this.navItem('📚', '诗词库', 'list', activePage, 'pages/PoemListPage')
this.navItem('👤', '作者', 'author', activePage, 'pages/AuthorPage')
this.navItem('⭐', '收藏', 'collection', activePage, 'pages/CollectionPage')
}
.width('100%').height(60)
.backgroundColor($r('app.color.bg_card'))
.padding({ top: 6, bottom: 6 })
.shadow({
radius: 8,
color: '#15000000',
offsetX: 0,
offsetY: -2 // 向上投影,浮在页面上方
})
3.2 Tab 高亮逻辑
当前页面对应的 Tab 使用主题色,其他 Tab 使用灰色:
Text(label).fontSize(10)
.fontColor(page === activePage ?
$r('app.color.accent_purple') : $r('app.color.text_secondary'))
3.3 Tab 点击跳转
点击非当前 Tab 时触发页面跳转,点击当前 Tab 不做任何操作:
.onClick(() => {
if (page !== activePage) {
router.pushUrl({ url: route });
}
})
四、ArkTS 中的响应式数据绑定
4.1 @State 装饰器
@State 是 ArkTS 中最基础的响应式装饰器,被修饰的变量变化时会触发 UI 重新渲染:
@Component
struct CollectionPage {
@State editMode: boolean = false;
@State filteredList: CollectionItem[] = myCollections;
// ...
}
4.2 @Watch 装饰器
@Watch 用于监听 @State 变量的变化,执行副作用逻辑:
@State @Watch('onGroupChange') selectedGroup: string = '';
关键点:@Watch 必须直接修饰在 @State 变量上,不能单独使用。当 selectedGroup 变化时,onGroupChange 方法自动被调用。
4.3 状态管理的完整流程
用户交互(点击分类标签)
│
▼
this.selectedGroup = '宋词'
│
├─→ UI 自动重渲染(分类标签高亮变化)
│
└─→ @Watch 触发 onGroupChange()
│
▼
执行过滤逻辑
│
▼
this.filteredList = [...]
│
└─→ UI 自动重渲染(收藏列表更新)
五、运行错误修复:get 访问器问题
在实际运行中,我们遇到了一个严重的运行时错误:
TypeError: Cannot read property length of undefined
错误根因:在 ArkTS 的动态模式下(arkTSMode: dynamic),get 访问器的返回值在模板绑定中无法被正确识别为响应式数据。当在 build() 方法中使用 this.filteredCollections.length 或 ForEach(this.filteredCollections, ...) 时,this.filteredCollections 返回的是 undefined。
解决方案:统一使用 @State + @Watch 替代 get 访问器:
| 页面 | 原方案 | 修复方案 |
|---|---|---|
CollectionPage |
get filteredCollections() |
@State filteredList + @Watch('onGroupChange') |
PoemListPage |
get filteredPoems() |
@State filteredList + @Watch('onFilterChange') |
AuthorPage |
get filteredAuthors() |
@State filteredAuthorsList + @Watch('onAuthorFilterChange') |
PoemDetailPage |
get poemData() |
@State poemData + @Watch('onPoemIdChange') |
这个修复经验非常重要——在 API 23 的 ArkTS 动态模式下,凡是需要在模板中使用的计算数据,都应该用 @State 存储,用 @Watch 触发更新,而不是依赖 get 访问器。
小结
本章完成了收藏页面的开发,实现了:
- 收藏数据的分组分类展示
- 分类筛选与高亮交互
- 编辑模式切换
- 全局底部导航栏的统一设计
- @State + @Watch 响应式数据管理的最佳实践
- get 访问器在动态模式下的问题与修复
至此,应用的所有 5 个页面已经开发完毕。下一章将总结整个开发过程中的编译错误修复和调试经验。
【系列目录】
- (一)项目初始化与架构设计
- (二)首页与诗词库页面开发
- (三)诗词详情与作者天地页面开发
- (四)收藏页面与底部导航实现 ← 本文
- (五)编译调试与问题修复经验
更多推荐



所有评论(0)