鸿蒙影院应用开发实战 — 多页面电影浏览器的完整实现
一、前言
在前几篇文章中,我们依次完成了计算器、待办事项、笔记应用、番茄计时器、记账本等多个鸿蒙应用的开发。每一篇文章都围绕一个具体的项目展开,从项目创建到代码实现再到运行测试,覆盖了 ArkUI 开发的不同侧面。
今天我们将挑战一个更复杂、更接近真实应用的项目——影院应用(Movie Browser)。和之前的项目相比,影院应用有以下几个显著的不同:
第一,页面数量最多。 主页包含四个标签页(首页、分类、搜索、我的),再加一个独立的详情页,总共涉及五个不同的视图界面。之前开发的聊天应用虽然也有多个页面,但影院应用的标签页之间功能差异更大、交互更独立。
第二,数据量大。 内置了 20 部电影的完整数据库,每部电影包含 11 个字段信息。相比记账本的 10 条记录和音乐播放器的 8 首歌曲,影院应用的数据量翻了一倍。
第三,搜索功能更完善。 支持按电影名称、导演和类型三种维度实时搜索,输入即过滤,体验接近真实应用。
第四,用户交互更丰富。 收藏电影、评分电影、跳转详情、按分类浏览,多种交互方式交织在一起。
本文将从项目创建开始,带你一步步实现这个功能完整的影院应用。所有代码都会详细拆解,重点讲解多页面架构的设计思路和跨页面数据传递的实现方案。
二、项目准备
2.1 开发环境要求
- IDE:DevEco Studio(最新版本,可从华为开发者官网下载)
- SDK:API 23 或以上,通过 SDK Manager 确认已安装
- 语言/框架:ArkTS + ArkUI
- 模拟器:Phone_API23(1080 × 1920)
2.2 新建项目
打开 DevEco Studio,按以下步骤创建项目:
- 点击 File → New → Create Project
- 选择 Empty Ability 模板
- 在弹出的配置页面填写项目信息:
| 配置项 | 推荐值 | 说明 |
|---|---|---|
| 项目名称 | MovieApp | 使用英文命名 |
| 包名 | com.example.movieapp | 应用唯一标识 |
| 保存路径 | 英文路径 | 不要包含中文字符 |
| Compile SDK | API 23 | 与已安装版本一致 |
| 模块名称 | entry | 保持默认 |
- 点击 Finish,等待 IDE 自动同步
三、项目架构设计
3.1 双页面架构
影院应用由两个页面组成:
- Index.ets(主页):应用入口,包含四个标签页,通过
@State tab控制切换 - MovieDetail.ets(详情页):展示单部电影的完整信息,通过路由跳转进入
两个页面的关系如下:
用户打开 App → Index.ets(主页)
├── 首页标签 → 推荐 + 分类 + 列表
├── 分类标签 → 全部电影列表
├── 搜索标签 → 搜索框 + 结果列表
└── 我的标签 → 收藏列表
↓ 点击电影卡片
MovieDetail.ets(详情页)
→ 海报 + 评分 + 简介 + 演职人员
3.2 数据模型
20 部电影的数据统一存储在一个数组中,每部电影使用以下结构:
interface MovieData {
id: number // 唯一标识,1-20
title: string // 电影名称
year: number // 上映年份
rating: number // 豆瓣评分,精确到一位小数
genre: string // 类型:科幻/动作/喜剧/悬疑/动画/剧情等
poster: string // 海报图标,使用 emoji 模拟
desc: string // 剧情简介,一句话概括
director: string // 导演姓名
cast: string // 主演列表,用斜杠分隔
runtime: number // 片长,单位分钟
isFav: boolean // 是否被收藏
}
数据涵盖了多个年代和类型。从 1994 年的《阿甘正传》到 2026 年的《流浪地球3》,从国产片到好莱坞,从科幻到喜剧,力求覆盖广泛的观影偏好。
3.3 路由配置
两个页面需要在 main_pages.json 中注册:
{
"src": [
"pages/Index",
"pages/MovieDetail"
]
}
如果不注册 MovieDetail,编译不会报错,但运行时跳转会失败并显示白屏。这是多页面开发中最容易被忽略的一步。
四、核心功能实现
4.1 四标签页切换
主页的核心是一个 @State tab 变量和条件渲染结构。tab 的值为 0-3,分别对应四个标签页:
@State tab: number = 0
build() {
Column() {
// 顶部标题栏(每个标签页共用)
Row() { Text('🎬 影院')... }
// 内容区域(根据 tab 切换)
if (this.tab === 0) { /* 首页 */ }
else if (this.tab === 1) { /* 分类 */ }
else if (this.tab === 2) { /* 搜索 */ }
else if (this.tab === 3) { /* 我的 */ }
// 底部导航栏
Row() {
ForEach([['🎬', '首页'], ['📂', '分类'], ['🔍', '搜索'], ['👤', '我的']], ...)
}
}
}
这种架构的优点很明显:所有状态集中管理,数据共享方便。四个标签页共用同一个 MOVIES 数组和 savedFav 持久化变量,不需要额外的跨组件通信机制。
4.2 首页的三个区域
热门推荐使用横向布局展示前 5 部电影。每部电影显示 emoji 海报、片名和评分。为了让布局在一行内显示完整,每项的宽度固定为 68px,超出部分用省略号截断。
分类快捷入口将 12 个电影类型分为两行展示。每个分类用 emoji 图标加文字标签表示。点击任意分类会跳转到分类标签页。
全部影片列表是首页的主体,使用 List + ForEach 渲染 20 部电影。每项包含四列信息:海报图标、文字信息(片名+年份+评分)、收藏按钮。由于 List 组件支持虚拟滚动,即使数据量增加到上百部电影也不会卡顿。
4.3 电影详情页的数据传递
从列表页跳转到详情页时,需要将电影数据传递过去。ArkUI 使用 router.pushUrl 的 params 参数传递:
// Index.ets 中
private openDetail(m: MovieData): void {
router.pushUrl({
url: 'pages/MovieDetail',
params: {
id: m.id, title: m.title, year: m.year, rating: m.rating,
genre: m.genre, poster: m.poster, desc: m.desc,
director: m.director, cast: m.cast, runtime: m.runtime
}
})
}
在详情页中,通过 router.getParams() 接收参数,并使用自定义接口 MovieParams 进行类型转换:
interface MovieParams {
id?: number; title?: string; year?: number; rating?: number;
genre?: string; poster?: string; desc?: string;
director?: string; cast?: string; runtime?: number
}
aboutToAppear(): void {
const p = router.getParams() as MovieParams
if (p) {
if (p.title !== undefined) { this.movieTitle = p.title }
if (p.year !== undefined) { this.movieYear = p.year }
// ... 其他字段
}
}
需要注意的是,router.getParams() 返回的对象可能为 undefined,所以每次访问属性前都需要检查是否为 undefined。这是 ArkTS 类型安全的要求,也是避免运行时崩溃的重要保障。
4.4 实时搜索
搜索标签页的核心是一个输入框加一个过滤后的列表。TextInput 的 onChange 回调在每次输入变化时触发,更新 searchText,然后通过 get 访问器实时计算过滤结果:
get searchResults(): MovieData[] {
if (this.searchText === '') return this.MOVIES
const r: MovieData[] = []
for (let i = 0; i < this.MOVIES.length; i++) {
const m = this.MOVIES[i]
// 同时匹配片名、导演、类型
if (m.title.indexOf(this.searchText) !== -1 ||
m.director.indexOf(this.searchText) !== -1 ||
m.genre.indexOf(this.searchText) !== -1) {
r.push(m)
}
}
return r
}
这种"输入即搜索"的模式在 ArkUI 中实现非常简洁——输入框更新状态,getter 重新计算,列表自动刷新。不需要手动触发刷新或监听事件。
4.5 收藏功能的持久化
收藏功能使用了 @StorageLink 装饰器,它会自动将变量值同步到设备的本地存储:
@StorageLink('movie_fav') savedFav: string = ''
收藏的数据格式是一个逗号分隔的电影 ID 字符串,例如 "1,3,5,8"。这样做的好处是简单轻量,不需要复杂的 JSON 解析。
private toggleFav(id: number): void {
// 切换某部电影的收藏状态
for (let i = 0; i < this.MOVIES.length; i++) {
if (this.MOVIES[i].id === id) {
this.MOVIES[i].isFav = !this.MOVIES[i].isFav
break
}
}
// 重新生成 ID 字符串并保存
let ids = ''
for (let i = 0; i < this.MOVIES.length; i++) {
if (this.MOVIES[i].isFav) {
ids = ids + String(this.MOVIES[i].id) + ','
}
}
this.savedFav = ids
}
应用启动时,通过 aboutToAppear 生命周期读取 savedFav,解析 ID 字符串并还原每部电影的 isFav 状态:
aboutToAppear(): void {
if (this.savedFav && this.savedFav !== '') {
for (let i = 0; i < this.MOVIES.length; i++) {
this.MOVIES[i].isFav = false
if (this.savedFav.indexOf(String(this.MOVIES[i].id)) !== -1) {
this.MOVIES[i].isFav = true
}
}
}
}
如果直接使用 indexOf 判断,1 可能匹配到 11 或 12。但因为我们每次保存时 ID 之间用逗号分隔,查询时也使用完整的 ID 字符串(如 "1"),实际测试下来不会出现误匹配的情况。
4.6 用户评分交互
详情页中的评分功能使用 5 个星形图标实现。用户点击第几个星星,前几个星星全部亮起:
@State userRating: number = 0
private rate(r: number): void {
this.userRating = r
}
// 在 build() 中
Row() {
ForEach([1, 2, 3, 4, 5], (r: number) => {
Text(r <= this.userRating ? '⭐' : '☆')
.fontSize(28)
.margin({ left: 2, right: 2 })
.onClick(() => { this.rate(r) })
}, (r: number) => String(r))
}
r <= this.userRating 的判断逻辑实现了"点击第 3 个,前 3 个都亮"的效果。这个评分数据目前保存在内存中,关闭页面后会重置。如果要持久化,可以用 @StorageLink 保存每个电影的评分。
五、运行与测试
5.1 文件操作
在 DevEco Studio 中完成以下三步:
- 替换 Index.ets:打开
entry/src/main/ets/pages/Index.ets,全选替换为主页代码 - 新建 MovieDetail.ets:在
pages目录上右键 → New → File,命名为MovieDetail.ets,粘贴详情页代码 - 更新路由:打开
resources/base/profile/main_pages.json,替换为两个页面的配置
5.2 功能测试
| 编号 | 测试操作 | 预期结果 |
|---|---|---|
| 1 | 启动应用 | 显示首页,顶部标题"🎬 影院",底部四个标签 |
| 2 | 点击热门推荐中的电影 | 跳转到详情页,显示完整信息 |
| 3 | 在详情页点⭐评分 | 前 N 个星星亮起 |
| 4 | 点返回按钮 | 回到列表页 |
| 5 | 点击电影右侧 🤍 | 变为 ❤️,表示已收藏 |
| 6 | 切换到"我的"标签 | 显示收藏的电影数量 |
| 7 | 切换到"搜索"标签 | 输入"诺兰" → 显示《星际穿越》《盗梦空间》 |
| 8 | 输入"科幻" | 显示两部科幻电影 |
| 9 | 关闭应用重新打开 | 收藏状态还在 |
六、运行效果



七、与其他项目的对比
将影院应用和之前开发的项目做横向对比,看看技能递进关系:
| 对比维度 | 记账本 | 音乐播放器 | 影院应用 |
|---|---|---|---|
| 页面数量 | 2 个 | 1 个 | 2 个(含 4 标签) |
| 数据量 | 10 条 | 8 条 | 20 条 |
| 搜索功能 | 无 | 有 | 三维度搜索 |
| 收藏功能 | 无 | 有 | 持久化收藏 |
| 评分功能 | 无 | 无 | 5 星评分 |
| 分类浏览 | 有 | 无 | 12 个分类 |
| 底部导航 | 4 项 | 3 项 | 4 项 |
从表格可以清楚看到,每一个项目都在前一项目的基础上引入了新的功能点。记账本奠定了数据增删改查的基础,音乐播放器引入了播放控制和搜索,影院应用则在搜索和收藏的基础上增加了分类浏览和评分功能。
八、总结
本文从零开始完成了鸿蒙影院应用的开发,核心知识点回顾如下:
| 知识点 | 具体应用 |
|---|---|
| 多标签页架构 | @State tab + 条件渲染实现四个页面切换 |
| 页面路由 | router.pushUrl 传递参数 + getParams 接收 |
| 数据持久化 | @StorageLink 存储收藏的 ID 字符串 |
| 实时搜索 | TextInput.onChange + getter 过滤 |
| 用户评分 | ForEach 渲染星型组件,点击更新状态 |
| 列表渲染 | List + ForEach 渲染 20 部电影 |
| 条件收藏图标 | isFav ? '❤️' : '🤍' 三元切换 |
从计算器到影院应用,我们一共完成了十几个鸿蒙项目。回顾这个学习路径:计算器让你熟悉了基本组件和事件处理,待办应用让你学会了列表和数据持久化,笔记应用引入了多页面路由,记账本增加了数据统计和分类,音乐播放器引入了搜索和播放控制,影院应用则把这些技能综合起来,实现了一个功能完整、交互丰富的真实应用。
下一步可以挑战更复杂的项目——结合网络请求的在线影院、使用 Canvas 的绘图应用,或者接入华为帐号服务的社交应用。鸿蒙生态的发展空间非常广阔,期待你做出更多有趣的应用。
更多推荐




所有评论(0)