第15次:首页完整实现

经过前面的学习,我们已经开发了 HeroBanner、ModuleCard 组件以及 TutorialService、ProgressService 服务。本次课程将整合所有内容,完成首页的完整实现。


首页效果

在这里插入图片描述

第15次:首页完整实现

本文讲解首页主框架的完整实现。这个页面不只是一个 Banner 入口,而是整个应用的控制中心:既负责初始化服务,又承载首页、课程、源码、项目、个人中心五个 Tab 的内容。


本篇先看懂什么

首页是最容易“越写越乱”的页面,因为它既有数据初始化,又有多区块组合,还要负责页面切换后的刷新。Index.ets 实际承担了下面这些职责:

  • 初始化主题、存储、教程数据、搜索服务、徽章服务、收藏服务。
  • 维护底部 Tab 切换。
  • 组织 HeroBanner、快捷入口、继续学习、推荐模块等首页区块。
  • 同时承载课程、源码学习、开源项目、个人中心几个内容面板。
  • 处理页面回显时的进度刷新。
  • 附带一个节日烟花动画层。

如果你先把这张结构图看明白,后面的代码阅读会轻松很多。

Index.ets

aboutToAppear

initTheme

initAndLoadData

StorageUtil.init

TutorialService.init

SearchService.init / BadgeService.init

ProgressService.loadProgress

Tabs

HomeContent

CourseContent

SourceCodeContent

OpenSourceContent

ProfileContent


一、先明确:首页不是“单页面”,而是一个容器页

很多初学者看到 Index.ets 会下意识认为它只是首页,但实际这个文件已经把应用里最重要的几个主视图都合并在了一起。也就是说,Index 更像一个“壳层页面”:

  • 底部 Tabs 负责大区域切换。
  • 每个 Tab 对应一个 @Builder
  • 子内容通过服务层获取数据,不在页面里硬编码。

这种写法的好处是切换底部 Tab 时状态比较集中,课程、进度、徽章这些数据都能在一个地方维护。缺点是文件会变长,所以你在阅读时要养成一个习惯:先从状态定义和生命周期看起,再跳到某个具体 Builder,千万不要一上来就从第一行一路读到最后。


二、真实初始化流程应该怎么理解

首页初始化的顺序如下:

  1. aboutToAppear() 先调用 initTheme(getContext(this))
  2. 然后执行 initAndLoadData()
  3. initAndLoadData() 内部先初始化本地存储。
  4. 再初始化 TutorialService,拿到全部模块数据。
  5. 用模块数据初始化 SearchServiceBadgeService
  6. 读取 ProgressService 里的学习进度。
  7. 加载收藏数据。
  8. 再根据当前进度检查是否需要发放徽章。
  9. 最后初始化源码学习和开源项目两个数据面板。

这套顺序不是随便排的。比如 BadgeService 依赖模块列表,SearchService 也依赖模块列表,如果你把它们放在 TutorialService.init() 之前,就会出现服务拿不到完整数据的问题。

把这段讲解对照到真实源码里看,会一下清楚很多:

private async initAndLoadData(): Promise<void> {
  try {
    await StorageUtil.init(getContext(this));
    TutorialService.init();
    this.modules = TutorialService.getAllModules();
    SearchService.init(this.modules);
    BadgeService.init(this.modules);
    this.progress = await ProgressService.loadProgress();
    await BookmarkService.loadBookmarks();
    await BadgeService.checkAndAwardBadges(this.progress);
    this.progress = await ProgressService.loadProgress();

    this.sourceCategories = SourceCodeData.getCategories();
    if (this.sourceCategories.length > 0) {
      this.selectedSourceCategory = this.sourceCategories[0].id;
      this.sourceChapters = SourceCodeData.getChaptersByCategory(this.selectedSourceCategory);
    }

    this.projectCategories = OpenSourceProjectData.getCategories();
    if (this.projectCategories.length > 0) {
      this.selectedProjectCategory = this.projectCategories[0].id;
      this.projects = OpenSourceProjectData.getProjectsByCategory(this.selectedProjectCategory);
    }
  } catch (error) {
    console.error('[Index] Failed to load data:', error);
  } finally {
    this.isLoading = false;
  }
}
BadgeService ProgressService TutorialService StorageUtil Index BadgeService ProgressService TutorialService StorageUtil Index init() init() modules = getAllModules() init(modules) loadProgress() checkAndAwardBadges(progress) 刷新 progress 与各 Tab 数据

三、首页区块是怎么拼出来的

真正的首页内容集中在 HomeContent() 中,它不是一个复杂算法,而是一个非常清晰的内容编排:

  • 顶部先放 HeroBanner
  • 然后是 QuickAccessSection
  • 如果存在 currentLesson,再显示 ContinueLearningSection
  • 最后放 RecommendedModulesSection

这说明首页设计遵循的是“先总览、再入口、再个性化、最后推荐”的顺序。这个顺序很适合学习类应用,因为用户打开应用最关心的是:

  1. 我学到哪了。
  2. 我现在能做什么。
  3. 有没有推荐内容。

ContinueLearningSection() 也不是凭空生成标题,而是直接读取 this.progress.currentLesson,再通过 findModuleIdByLessonId() 找回所属模块,最后跳转到 LessonDetail。这也是为什么上一节我们要强调进度服务里 currentLesson 字段的重要性。

对应到 HomeContent(),正文顺序就是这样一层层排下来的:

@Builder
HomeContent() {
  Scroll() {
    Column() {
      HeroBanner({
        completedLessons: this.progress.completedLessons.length,
        totalLessons: this.getTotalContentCount(),
        streak: this.progress.learningStreak,
        onDailyQuestionTap: () => {
          router.pushUrl({ url: 'pages/QuizPage' });
        }
      })

      this.QuickAccessSection()

      if (this.progress.currentLesson) {
        this.ContinueLearningSection()
      }

      this.RecommendedModulesSection()
    }
  }
  .width('100%')
  .height('100%')
  .scrollBar(BarState.Off)
}

四、底部 Tab 的实现思路值得你照着学

当前项目使用 Tabs + TabContent + 自定义 tabBar Builder 的方式来实现底部导航。这样做的优势是:

  • 图标和文字状态可以自己控制。
  • 明暗主题切换时颜色可以统一处理。
  • 后续要加徽章角标、动态样式时扩展更方便。

TabBuilder(title, icon, index) 这个方法虽然短,但作用很大。因为它把“当前是否选中”的判断统一收口了:

  • 当前 Tab:使用亮色主题色。
  • 非当前 Tab:使用灰色弱化。

你以后在鸿蒙里做底部导航时,不一定每次都要依赖复杂组件库。像这样用 Builder 自己拼,反而更适合项目化开发,样式和业务状态都抓得更牢。


五、课程、源码、项目、个人中心为什么也放在这个文件里

从工程拆分角度看,这个文件确实比较长,但它也有一个好处:你可以非常直观地看到整个应用主框架。

1. CourseContent()

这里按难度把所有模块分组展示。难度顺序是固定的:beginnerbasicintermediateadvancedecosystem。每个模块项点击后跳转到 ModuleDetail

2. SourceCodeContent()

这一块不是跳到新页面,而是在首页内直接展示源码学习分类、章节列表、展开详情,再通过按钮进入 SourceCodeDetailPage

3. OpenSourceContent()

结构和源码学习类似,只是数据源换成 OpenSourceProjectData,内容换成项目特性、安装方式、快速开始和相关链接。

4. ProfileContent()

个人中心展示学习统计、徽章列表和若干功能入口,比如收藏、搜索、每日一题、面试题库。它本质上就是“进度数据的消费页”。

所以你可以把 Index.ets 理解成一个主舞台:不同数据服务在后台提供数据,不同 Builder 负责前台渲染。


六、读这一页时要特别看清五个主 Tab

真正把首页、课程、源码、项目、我的串起来的,不是五个分散页面,而是 build() 里的五个 TabContent。读这一段时,你要把它当作应用主框架来看,而不是把它误以为只是一个普通首页。

TabContent() {
  this.HomeContent()
}
.tabBar(this.TabBuilder('首页', '🏠', 0))

TabContent() {
  this.CourseContent()
}
.tabBar(this.TabBuilder('课程', '📚', 1))

TabContent() {
  this.SourceCodeContent()
}
.tabBar(this.TabBuilder('源码', '📖', 2))

TabContent() {
  this.OpenSourceContent()
}
.tabBar(this.TabBuilder('项目', '🌟', 3))

TabContent() {
  this.ProfileContent()
}
.tabBar(this.TabBuilder('我的', '👤', 4))

这一段代码至少说明了三件事:

  • 五个主功能共用同一个页面壳层。
  • 每个区域都有独立 Builder,结构清楚。
  • 源码学习、开源项目、个人中心都和首页共享初始化后的服务数据。

继续往下读时,再留意两个容易忽略的点:

  • onPageShow() 会在页面回显后刷新进度,保证从详情页返回时首页统计是新的。
  • 烟花动画状态放在页面内部管理,不会污染全局服务层。

七、你可以怎样自己动手验证这一页

建议你不要一次看完 1000 多行 Index.ets,而是按下面顺序操作:

  1. 先只看顶部状态字段,搞清楚它管理了哪些 Tab、哪些服务数据、哪些动画状态。
  2. 再看 aboutToAppear()initAndLoadData(),把初始化顺序记下来。
  3. 只看 HomeContent(),理解首页区块怎么拼。
  4. 再看 CourseContent(),理解模块列表如何按难度生成。
  5. 最后分别看 SourceCodeContent()OpenSourceContent()ProfileContent()

如果你这样分段阅读,会明显比“硬啃一个大文件”轻松很多。


八、写首页时最容易踩的坑

1. 在页面里乱写初始化顺序

服务之间有依赖关系,必须先初始化存储,再初始化教程数据,再初始化依赖模块数据的服务。

2. 页面返回后不刷新进度

如果缺少 onPageShow() 里的 refreshProgress(),那你在课程详情页完成学习后,首页统计不会及时变化。

3. 把所有 UI 都写成一个超大 build()

当前项目虽然文件长,但它至少把内容拆到了多个 Builder 中。你要延续这种拆法,而不是把所有 Row、Column 堆在一起。

4. 只关注首页,忽略它还是整个应用的容器

Index 不只是“首页展示页”,它还是整个主框架的状态中心。这一点如果理解错了,后面做扩展就会非常吃力。


本篇小结

真实项目里的首页,核心不在“放几个漂亮卡片”,而在于它要承担应用主框架的职责。你这次应该重点记住三件事:

  • Index.ets 是容器页,不只是首页。
  • 初始化顺序要严格按依赖关系来。
  • 首页、课程、源码、项目、个人中心其实共享同一套主状态和服务数据。

理解这一点之后,下一篇我们再单独拆开看课程列表区块,你就会发现首页里那部分代码其实没那么吓人。


课后练习

  1. 在不改动服务层的前提下,自己画一张 Index.ets 的组件结构图。
  2. 观察 HomeContent()ProfileContent() 都读取了哪些 progress 字段,整理成表格。
  3. 思考为什么 Index.ets 里的烟花动画状态不适合放进全局服务,而更适合放在页面内部状态中。
Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐