一、前言

在前几篇文章中,我们依次完成了计算器、待办事项、笔记应用、番茄计时器、记账本等多个鸿蒙应用的开发。每一篇文章都围绕一个具体的项目展开,从项目创建到代码实现再到运行测试,覆盖了 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,按以下步骤创建项目:

  1. 点击 File → New → Create Project
  2. 选择 Empty Ability 模板
  3. 在弹出的配置页面填写项目信息:
配置项 推荐值 说明
项目名称 MovieApp 使用英文命名
包名 com.example.movieapp 应用唯一标识
保存路径 英文路径 不要包含中文字符
Compile SDK API 23 与已安装版本一致
模块名称 entry 保持默认
  1. 点击 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.pushUrlparams 参数传递:

// 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 实时搜索

搜索标签页的核心是一个输入框加一个过滤后的列表。TextInputonChange 回调在每次输入变化时触发,更新 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 可能匹配到 1112。但因为我们每次保存时 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 中完成以下三步:

  1. 替换 Index.ets:打开 entry/src/main/ets/pages/Index.ets,全选替换为主页代码
  2. 新建 MovieDetail.ets:在 pages 目录上右键 → New → File,命名为 MovieDetail.ets,粘贴详情页代码
  3. 更新路由:打开 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 的绘图应用,或者接入华为帐号服务的社交应用。鸿蒙生态的发展空间非常广阔,期待你做出更多有趣的应用。

Logo

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

更多推荐