小芽英语鸿蒙开发实战 系列1:全栈架构设计与鸿蒙 Navigation 路由深层博弈
背景介绍
最近家里的孩子上幼儿园了,给报名了个英语延时班,老师要求每天打卡复习。作为老父亲,由于担心自己的“工地英语”发音不标准不敢乱教孩子,每天都只能借助一些现成的 AI 工具来发音让孩子跟读。
但在实际使用中,痛点接踵而至:非常不方便。有时候句子长,或者单词比较拗口的话,孩子根本跟不上;你想让它念得慢点儿,很多工具压根不支持语速调节。
作为一名程序员,遇到痛点当然是自己动手解决。我突然灵机一动:干嘛不自己开发个鸿蒙应用?每天输入每节课的生词和句子,借助鸿蒙系统原生强大的 TTS(文本转语音)能力进行播放,想要多慢就调多慢,让孩子轻松跟读。
需求一旦打开,就一发不可收拾:既然都做了跟读,干嘛不更完善些,把发音打分功能也做上?说干就干!我直接给这个项目起名叫“小芽英语”,专为幼儿园孩子量身定制的英语学习打卡神器。
从今天开始,我将把这个应用的完整开发历程——从零代码到端云协同全栈落地,整理成一个系列实战专栏。希望这份真实的“踩坑与避坑指南”,能为正在探索 HarmonyOS NEXT 领域的你提供一些极客视角的参考。
1. 架构纵览:端云协同与职责边界
在敲下第一行代码之前,我们必须厘清系统的边界。“小芽英语”虽然界面极简,但其五脏俱全,是一套典型的端云协同系统。
在这个架构中:
- 鸿蒙客户端:负责儿童友好的沉浸式 UI 展示、调用系统底层 TTS 提供可调速的外教发音、以及接管麦克风录制跟读音频。
- Java 服务端:基于 Spring Boot 构建,作为安全的网关与 AI 中枢。负责接收来自客户端的 PCM 音频裸流,对接云端的高级语音评测引擎,并将打分结果返回给端侧。
本篇文章,我们将聚焦于鸿蒙端的地基建设:如何规划目录结构,以及如何驾驭鸿蒙推荐的最新路由引擎 —— Navigation。
2. 工程地基:清晰的目录结构
在很多初学者的 Demo 中,喜欢把 UI 和逻辑全塞进pages目录下。但作为一个商业级项目,我们必须一开始就做到职责分离:
entry/src/main/ets/ ├── pages/ # 页面层:只包含 UI 布局和基础状态维护 │ ├── Index.ets # 首页:身份选择 (家长端/儿童端) │ ├── ChildEntry.ets # 儿童端:卡片跟读、大按钮录音 │ └── ParentEntry.ets # 家长端:表单录入文本 ├── utils/ # 工具层:底层能力的封装 │ ├── AudioRecorder.ets # 麦克风裸流采集封装 │ ├── ScoringApiClient.ets# HTTP 二进制流网络封装 │ └── TTSManager.ets # 文本转语音引擎单例 └── ...这种结构的好处是:UI 层不需要关心 PCM 是怎么采样的,也不需要知道网络是怎么重试的。pages里的组件只管“长得好看”和“响应点击”。
3. 路由博弈:摒弃 Router,全面拥抱 Navigation
有了页面后,如何把它们串联起来?
在鸿蒙早期版本中,大家习惯使用基于系统 Window 层级的@ohos.router。但从 API 10 开始,华为官方强烈推荐使用组件级的Navigation。
为什么?因为Router每次跳转都会加载完整的页面级环境,开销极大,且无法很好地适应未来多设备(折叠屏、平板)的分栏适配。而Navigation作为 ArkUI 的一个普通组件,它的路由跳转本质上只是组件树的局部替换,性能有着质的飞跃。
3.1 搭建根路由栈
在Index.ets(我们的入口页面)中,我们首先要初始化一个路由栈NavPathStack,并通过依赖注入(@Provide)将其共享给所有子页面。
import{ChildEntry}from'./ChildEntry';import{ParentEntry}from'./ParentEntry';@Entry@Componentstruct Index{// 【核心机制】:创建路由栈并下发,子页面可通过 @Consume 直接获取@Provide('pageStack')pageStack:NavPathStack=newNavPathStack();// 路由分发大本营@BuilderPageMap(name:string){if(name==='ChildEntry'){ChildEntry()}elseif(name==='ParentEntry'){ParentEntry()}}build(){Navigation(this.pageStack){Column({space:30}){// ... 欢迎界面与身份选择按钮Button('进入儿童跟读').onClick(()=>{// 路由跳转this.pageStack.pushPathByName('ChildEntry',null);})}.width('100%').height('100%')}// 隐藏自带的标题栏,为了后面实现沉浸式 UI.hideTitleBar(true).navDestination(this.PageMap)}}底层思路解析:
在这段代码中,Navigation相当于一个容器,navDestination是路由的“调度员”。当我们调用pushPathByName时,框架会去PageMap这个建造者(Builder)中寻找匹配的名字,然后原地渲染出ChildEntry组件。整个过程不需要通过系统的 WindowManager,因此非常轻量级。
3.2 优雅的安全回退
对于跳转过去的子页面(例如ChildEntry.ets),它不能像过去使用router.back()那样直接调系统 API,而是要使用注入进来的路由栈进行回退。
@Componentexportstruct ChildEntry{// 【核心机制】:通过 @Consume 接收 Index 抛下来的路由栈@Consume('pageStack')pageStack:NavPathStack;build(){NavDestination(){Column(){// 自定义沉浸式返回按钮Image($r('app.media.icon_back')).width(32).height(32).onClick(()=>{// 安全弹栈this.pageStack.pop();})// ... 跟读卡片等业务 UI}}.hideTitleBar(true)}}避坑指南:使用Navigation时,被路由跳转的子页面最外层必须是NavDestination组件。这是初学者最容易踩坑的地方——如果你最外层直接写个Column,页面是绝对跳不过来的,并且鸿蒙编译器有时还不会给你明确的报错。
4. 总结
带着为孩子解决真实痛点的初衷,我们的“小芽英语”正式起航。在本篇开局之战中,我们确立了端云协同的底层架构,规划了职责分明的工程目录,并果断抛弃了老旧的 Router,拥抱了代表未来的 Navigation 组件级路由。我们使用依赖注入 (@Provide/@Consume) 的方式,构建了一条干净、安全、高性能的页面导航总线。
但这只是个空架子,一个给儿童用的应用,如果全屏都是灰白色的默认背景,那简直是一场灾难。在下一篇文章中,我们将直面 ArkUI 的渲染引擎,解析如何通过沉浸式属性(expandSafeArea)打破屏幕边界,打造出充满童趣的极客排版与拟物卡片!在这里插入图片描述