HarmonyOS Next 项目完整学习指南
HarmonyOS Next 项目完整学习指南
基于 HealthyMate(泰科健康)项目的实战学习文档
适合人群: 完成 HarmonyOS Next 基础学习的初学者
学习目标: 通过真实项目巩固基础知识,掌握实际开发技能
📚 目录
- 第一章:项目概览
- 第二章:核心概念速览
- 第三章:项目结构详解
- 第四章:从零开始 - 启动流程
- 第五章:UI组件开发
- 第六章:状态管理
- 第七章:路由导航
- 第八章:数据模型
- 第九章:图表组件
- 第十章:最佳实践
- 第十一章:常见问题
第一章:项目概览
1.1 项目简介
**HealthyMate(泰科健康)**是一款基于 HarmonyOS Next 开发的综合性健康管理应用。
核心功能模块:
- 🏠 首页模块: 健康检测、医院查询、热点资讯
- 🔍 健康发现: 健康知识、视频播放、管理工具
- 📊 健康数据: 数据展示、趋势分析、图表可视化
- 👤 个人中心: 用户信息、设置管理
技术栈:
开发框架: HarmonyOS Next
开发语言: ArkTS (TypeScript 扩展)
UI框架: ArkUI (声明式UI)
开发工具: DevEco Studio
包管理: ohpm
1.2 学习路线图
启动流程 → UI组件 → 状态管理 → 路由导航 → 数据交互 → 高级特性↓ ↓ ↓ ↓ ↓ ↓Entry @Component @State Navigation Model 第三方库Ability @Builder AppStorage router Class mpchart
第二章:核心概念速览
2.1 ArkTS 基础概念
2.1.1 装饰器系统
ArkTS 使用装饰器来增强类、组件和变量的功能:
装饰器 | 作用 | 使用场景 | 示例 |
---|---|---|---|
@Entry | 标记页面入口 | 可独立运行的页面 | 启动页、登录页 |
@Component | 定义自定义组件 | 可复用的UI组件 | 卡片、按钮 |
@State | 状态变量 | 组件内部状态 | 计数器、开关 |
@Prop | 单向数据传递 | 父组件→子组件 | 配置参数 |
@Link | 双向数据绑定 | 父子组件共享 | 表单数据 |
@Builder | 自定义构建函数 | 动态UI内容 | 插槽内容 |
@BuilderParam | 插槽参数 | 接收UI内容 | 卡片插槽 |
@Extend | 扩展组件样式 | 统一样式 | 文本样式 |
@Preview | 预览组件 | 开发调试 | 组件预览 |
2.1.2 核心组件
容器组件:
Column() // 垂直布局
Row() // 水平布局
Stack() // 堆叠布局
Flex() // 弹性布局
Grid() // 网格布局
List() // 列表布局
Swiper() // 轮播容器
Tabs() // 标签页容器
基础组件:
Text() // 文本
Image() // 图片
Button() // 按钮
TextInput() // 文本输入
2.2 项目架构概念
2.2.1 应用生命周期
应用启动↓
EntryAbility.onCreate() // Ability 创建↓
onWindowStageCreate() // 窗口创建↓
loadContent('pages/SplashPage') // 加载启动页↓
页面显示↓
aboutToAppear() // 页面即将显示↓
build() // 构建UI↓
onPageShow() // 页面已显示
2.2.2 组件生命周期
@Component
struct MyComponent {// 1. 组件即将创建aboutToAppear() {console.log('组件即将出现')// 初始化数据、启动定时器}// 2. 构建UI(每次状态变化都会触发)build() {Column() {Text('Hello')}}// 3. 组件即将销毁aboutToDisappear() {console.log('组件即将消失')// 清理定时器、释放资源}
}
第三章:项目结构详解
3.1 目录树结构
HealthyMate/
├── AppScope/ # 应用全局配置
│ ├── app.json5 # 应用配置文件
│ └── resources/ # 全局资源
│
├── entry/ # 主模块
│ ├── src/main/
│ │ ├── ets/ # ArkTS 源代码
│ │ │ ├── entryability/ # 应用入口
│ │ │ │ └── EntryAbility.ets
│ │ │ │
│ │ │ ├── pages/ # 页面文件
│ │ │ │ ├── Index.ets # 主页面
│ │ │ │ ├── SplashPage.ets # 启动页
│ │ │ │ ├── GuidePage.ets # 引导页
│ │ │ │ ├── LoginPage.ets # 登录页
│ │ │ │ │
│ │ │ │ ├── comp/ # 公共组件
│ │ │ │ │ ├── HomeComp.ets # 首页组件
│ │ │ │ │ ├── CardComp.ets # 卡片组件
│ │ │ │ │ ├── TopNavComp.ets # 导航栏
│ │ │ │ │ └── ...
│ │ │ │ │
│ │ │ │ ├── model/ # 数据模型
│ │ │ │ │ ├── NavCard.ets # 导航卡片模型
│ │ │ │ │ └── HealthyData.ets # 健康数据模型
│ │ │ │ │
│ │ │ │ ├── view/ # 视图组件
│ │ │ │ │ ├── LineCharts.ets # 折线图
│ │ │ │ │ └── BarCharts.ets # 柱状图
│ │ │ │ │
│ │ │ │ ├── home/ # 首页相关页面
│ │ │ │ ├── healthydiscover/ # 健康发现页面
│ │ │ │ └── profile/ # 个人中心页面
│ │ │ │
│ │ │ └── utils/ # 工具类
│ │ │
│ │ ├── resources/ # 资源文件
│ │ │ ├── base/ # 基础资源
│ │ │ │ ├── element/ # 元素资源
│ │ │ │ │ ├── string.json # 字符串资源
│ │ │ │ │ ├── color.json # 颜色资源
│ │ │ │ │ └── float.json # 尺寸资源
│ │ │ │ ├── media/ # 媒体资源(图片等)
│ │ │ │ └── profile/ # 配置文件
│ │ │ │ └── main_pages.json
│ │ │ │ └── route_map.json
│ │ │ ├── zh_CN/ # 中文资源
│ │ │ └── en_US/ # 英文资源
│ │ │
│ │ └── module.json5 # 模块配置文件
│ │
│ └── build-profile.json5 # 构建配置
│
├── oh_modules/ # 第三方依赖
│ └── @ohos/
│ └── mpchart/ # 图表库
│
├── oh-package.json5 # 包配置文件
└── hvigorfile.ts # 构建脚本
3.2 关键文件说明
3.2.1 module.json5
- 模块配置
{"module": {"name": "entry","type": "entry","routerMap": "$profile:route_map","mainElement": "EntryAbility","pages": "$profile:main_pages",// 权限声明"requestPermissions": [{"name": "ohos.permission.INTERNET" // 网络权限},{"name": "ohos.permission.LOCATION" // 位置权限}]}
}
重要配置项:
mainElement
: 指定应用入口 Abilitypages
: 页面配置文件路径routerMap
: 路由配置文件路径requestPermissions
: 权限声明列表
3.2.2 main_pages.json
- 页面配置
{"src": ["pages/SplashPage","pages/GuidePage","pages/Index","pages/LoginPage"]
}
3.2.3 route_map.json
- 路由配置
{"routerMap": [{"name": "HealthCheckPage","pageSourceFile": "src/main/ets/pages/home/HealthCheckPage.ets"}]
}
第四章:从零开始 - 启动流程
4.1 应用入口:EntryAbility
文件位置: entry/src/main/ets/entryability/EntryAbility.ets
import { AbilityConstant, UIAbility, Want } from '@kit.AbilityKit';
import { window } from '@kit.ArkUI';export default class EntryAbility extends UIAbility {// 1. Ability 创建时调用onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {console.log('应用创建')}// 2. 窗口阶段创建onWindowStageCreate(windowStage: window.WindowStage): void {// 加载启动页windowStage.loadContent('pages/SplashPage', (err) => {if (err.code) {console.error('加载页面失败:', err)return;}console.log('页面加载成功')});}// 3. 前台onForeground(): void {console.log('应用进入前台')}// 4. 后台onBackground(): void {console.log('应用进入后台')}// 5. 销毁onDestroy(): void {console.log('应用销毁')}
}
知识点总结:
- ✅
UIAbility
是应用/服务的基类 - ✅
onWindowStageCreate
中加载首页 - ✅ 通过
loadContent
指定启动页面
4.2 启动页实现:SplashPage
文件位置: entry/src/main/ets/pages/SplashPage.ets
import { router } from "@kit.ArkUI";@Entry
@Component
struct SplashPage {@State countDown: number = 5 // 倒计时秒数@State isAutoJump: boolean = true // 是否自动跳转private timerID: number = 0 // 定时器ID// 页面显示时启动定时器aboutToAppear() {this.startCountdown()}// 页面隐藏时清除定时器aboutToDisappear() {clearInterval(this.timerID)}// 启动倒计时private startCountdown() {this.timerID = setInterval(() => {if (this.countDown > 0) {this.countDown -= 1 // 每秒减1} else {clearInterval(this.timerID) // 清除定时器this.jumpToHome() // 跳转}}, 1000)}// 手动跳过private skipGuide() {clearInterval(this.timerID)this.jumpToHome()}// 跳转到引导页private jumpToHome() {router.pushUrl({ url: 'pages/GuidePage' })}build() {Stack({ alignContent: Alignment.TopEnd }) {// 主内容区域Column() {Column({ space: 12 }) {Image($r("app.media.icon_app")).width(140).height(160)Text('泰克健康').fontSize(38).fontColor('#000000').margin({ top: 10 })Text('— 安全守护 —').fontSize(38).fontColor('#A5ABCE')}.width(238).height(328)}.justifyContent(FlexAlign.Center).height('100%').width('100%')// 右上角跳过按钮Button() {Text('跳过 ' + this.countDown.toString()).fontSize(18).fontColor('#FFFFFF')}.margin({ top: 20, right: 12 }).padding({ left: 10, top: 5, right: 10, bottom: 5 }).borderRadius(15).backgroundColor('#33000000').onClick(() => {this.skipGuide()})}}
}
知识点详解:
4.2.1 装饰器用法
@Entry // 标记为页面入口,可以独立运行
@Component // 标记为自定义组件
struct SplashPage { }
4.2.2 状态管理
@State countDown: number = 5
// @State 装饰的变量改变时,会自动触发UI刷新
4.2.3 生命周期钩子
aboutToAppear() // 组件即将出现(页面加载后)
aboutToDisappear() // 组件即将消失(页面销毁前)
4.2.4 定时器使用
// 启动定时器
this.timerID = setInterval(() => {// 每秒执行
}, 1000)// 清除定时器(重要!防止内存泄漏)
clearInterval(this.timerID)
4.2.5 Stack 布局
Stack({ alignContent: Alignment.TopEnd }) {// 子组件会堆叠在一起Column() { } // 第一层:主内容Button() { } // 第二层:浮动按钮
}
Stack 对齐方式:
Alignment.TopStart
- 左上角Alignment.TopEnd
- 右上角Alignment.Center
- 居中Alignment.BottomEnd
- 右下角
4.3 引导页实现:GuidePage
文件位置: entry/src/main/ets/pages/GuidePage.ets
import GuideComp from './comp/GuideComp'
import { router } from '@kit.ArkUI'// 引导页数据接口
interface GuideItem {currentIndex: numberguideText1: stringguideText2: stringguideImage: ResourceStrisDisplayBtn?: boolean
}// 引导页数据
const guideArrays: GuideItem[] = [{currentIndex: 0,guideText1: '智能检测',guideText2: '让健康生活触手可及',guideImage: $r('app.media.guide01')},{currentIndex: 1,guideText1: '按时服药,健康相伴',guideText2: '您的智能健康提醒专家',guideImage: $r('app.media.guide02')},{currentIndex: 2,guideText1: '您的个人健康档案',guideText2: '一目了然',guideImage: $r('app.media.guide03'),isDisplayBtn: true // 最后一页显示按钮}
]@Entry
@Component
struct GuidePage {@State currentIndex: number = 0@State guideList: GuideItem[] = guideArraysbuild() {Swiper() {ForEach(this.guideList, (item: GuideItem, index) => {Column() {// 引导内容组件GuideComp({currentIndex: item.currentIndex,guideText1: item.guideText1,guideText2: item.guideText2,guideImage: item.guideImage})// 最后一页显示按钮if (index === this.guideList.length - 1) {Button('立即开启', { type: ButtonType.Normal }).onClick(() => {router.pushUrl({ url: 'pages/RegisterPage' })}).padding({ top: 12, bottom: 12 }).width('88%').borderRadius(12).linearGradient({angle: 180,colors: [[0x9AE0FF, 0.0],[0x526AF3, 1.0]]})}}.width('100%').height('100%')})}.loop(false) // 不循环.autoPlay(false) // 不自动播放.cachedCount(3) // 缓存页面数.disableSwipe(this.currentIndex === this.guideList.length - 1).indicator( // 指示器样式Indicator.dot().itemWidth(15).itemHeight(15).selectedItemWidth(30).selectedItemHeight(15).selectedColor(0x5F80F5)).onChange((index: number) => {this.currentIndex = index // 更新当前索引})}
}
知识点详解:
4.3.1 Swiper 轮播组件
Swiper() {// 子组件
}
.loop(false) // 是否循环
.autoPlay(false) // 是否自动播放
.indicator() // 指示器
.onChange() // 页面切换回调
4.3.2 ForEach 列表渲染
ForEach(arr: Array<T>, // 数据源itemGenerator: (item: T, index: number) => void, // 渲染函数keyGenerator?: (item: T, index: number) => string // 键生成函数
)// 示例
ForEach(this.guideList, (item: GuideItem, index) => {Text(item.guideText1)
}, (item: GuideItem) => item.currentIndex.toString())
4.3.3 条件渲染
if (condition) {// 条件为真时显示Button('显示')
}
4.3.4 渐变效果
.linearGradient({angle: 180, // 渐变角度colors: [[Color1, 0.0], // 起始颜色和位置[Color2, 1.0] // 结束颜色和位置]
})
第五章:UI组件开发
5.1 主页面结构:Index
文件位置: entry/src/main/ets/pages/Index.ets
import HomeComp from './comp/HomeComp'
import DiscoverComp from './comp/DiscoverComp'
import DataComp from './comp/DataComp'
import ProfileComp from './comp/ProfileComp'// 设置全局状态
AppStorage.setOrCreate('CurrentTabIndex', 0);
AppStorage.setOrCreate('IsLoggedIn', false);@Entry
@Component
struct Index {// 创建路由栈pathStack: NavPathStack = new NavPathStack();aboutToAppear(): void {// 将路由栈存储到全局AppStorage.setOrCreate("pathStack", this.pathStack);}@State currentIndex: number = 0// 底部导航图标private navIcons = [$r('app.media.icon_home_svg'),$r('app.media.icon_discover_svg'),$r('app.media.icon_data_svg'),$r('app.media.icon_profile_svg')]build() {Navigation(this.pathStack) {Tabs({ barPosition: BarPosition.End }) {TabContent() {HomeComp()}.tabBar(this.tabBarBuilder('首页', 0))TabContent() {DiscoverComp()}.tabBar(this.tabBarBuilder('健康发现', 1))TabContent() {DataComp()}.tabBar(this.tabBarBuilder('健康数据', 2))TabContent() {ProfileComp()}.tabBar(this.tabBarBuilder('个人中心', 3))}.onChange((index: number) => {this.currentIndex = index})}.mode(NavigationMode.Stack).hideTitleBar(true)}// 自定义 TabBar 构建函数@BuildertabBarBuilder(name: string, index: number) {Column() {Image(this.navIcons[index]).fillColor(this.currentIndex === index ? '#4D91FF' : '#A9B7D8').width(25).height(25)Text(name).fontSize(14).fontColor(this.currentIndex === index ? '#677DF7' : '#A9B7D8')}}
}
知识点详解:
5.1.1 Tabs 标签页组件
Tabs({ barPosition: BarPosition.End }) { // 底部标签栏TabContent() {// 页面内容}.tabBar(自定义TabBar)
}
.onChange((index: number) => {// 标签切换回调
})
BarPosition 枚举:
BarPosition.Start
- 顶部BarPosition.End
- 底部
5.1.2 Navigation 导航容器
Navigation(pathStack) {// 页面内容
}
.mode(NavigationMode.Stack) // 堆栈模式
.hideTitleBar(true) // 隐藏标题栏
5.1.3 @Builder 自定义构建函数
@Builder
tabBarBuilder(name: string, index: number) {Column() {Image(...)Text(name)}
}// 使用
.tabBar(this.tabBarBuilder('首页', 0))
@Builder 要点:
- ✅ 可以接收参数
- ✅ 可以在
build()
方法外定义 - ✅ 可以复用UI结构
5.2 自定义组件:CardComp
文件位置: entry/src/main/ets/pages/comp/CardComp.ets
class NavItem {img: ResourceStrtitle: stringsubtitle?: stringconstructor(img: ResourceStr, title: string, subtitle?: string) {this.img = imgthis.title = titlethis.subtitle = subtitle}
}@Component
export default struct CardComp {// 组件属性wid: number | string = '100%'gradientStart?: stringgradientEnd?: stringpaddingValue?: Padding | Length | LocalizedPadding = 12// 接收父组件传递的数据@Prop params: NavItem// 插槽参数:接收父组件传递的UI构建函数@BuilderParam contentBuilder: (params: NavItem) => void = initBuilderbuild() {Column() {// 调用传入的构建函数this.contentBuilder(this.params)}.borderRadius(12).padding(this.paddingValue).width(this.wid).linearGradient(this.gradientStart && this.gradientEnd ? {direction: GradientDirection.Right,colors: [[this.gradientStart, 0.0],[this.gradientEnd, 1.0]]} : { colors: [] }).backgroundColor(this.gradientStart ? Color.Transparent : '#FFFFFF')}
}// 默认构建函数
@Builder
function initBuilder() {Text('默认占位符')
}
使用示例:
// 父组件中使用
CardComp({wid: '100%',gradientStart: '#CCF2FF',gradientEnd: '#FFFFFF',paddingValue: 5,params: myNavItem, // 传递数据contentBuilder: itemContent // 传递UI构建函数
})// 自定义内容构建函数
@Builder
function itemContent(param: NavItem) {Row({ space: 2 }) {Image(param.img).width(44).height(44)Text(param.title).fontSize(14)}.width('100%')
}
知识点详解:
5.2.1 @Prop 单向数据传递
@Prop params: NavItem// 特点:
// ✅ 父组件 → 子组件 单向传递
// ✅ 子组件修改不影响父组件
// ✅ 支持深拷贝
5.2.2 @BuilderParam 插槽
@BuilderParam contentBuilder: (params: NavItem) => void = defaultBuilder// 特点:
// ✅ 允许父组件自定义子组件的部分UI
// ✅ 类似 Vue 的 slot、React 的 children
// ✅ 可以传递参数
@BuilderParam 传参规则:
// ❌ 错误:直接调用
contentBuilder: this.myBuilder()// ✅ 正确:传递函数引用
contentBuilder: this.myBuilder// ✅ 正确:传递全局Builder
contentBuilder: globalBuilder
5.2.3 条件样式
.linearGradient(condition ? styleA : styleB
)// 示例
.backgroundColor(this.gradientStart ? Color.Transparent : '#FFFFFF'
)
5.3 复合组件:HomeComp
文件位置: entry/src/main/ets/pages/comp/HomeComp.ets
import TopNavComp from '../comp/TopNavComp'
import CardComp from '../comp/CardComp'
import { NavItem, navItems, lifeReminder, checkUp } from '../model/NavCard'@Component
export default struct HomeComp {// 获取全局路由栈pathStack: NavPathStack = AppStorage.get("pathStack") as NavPathStack;build() {NavDestination() {Column() {// 1. 顶部导航栏TopNavComp({ title: '泰克健康' })Column() {// 2. 功能网格Grid() {ForEach(navItems, (item: NavItem, index) => {GridItem() {CardComp({wid: '50%',params: item,contentBuilder: navItemContent})}.onClick(() => {this.pathStack.pushPathByName(item.url, null)})})}.rowsTemplate('1fr 1fr') // 2行.rowsGap(5).columnsGap('2%').height('24%')// 3. 每日签到区域Text('每日签到').fontSize(21).fontWeight(FontWeight.Bold)Column() {Stack({ alignContent: Alignment.Center }) {// 背景层Column().width('100%').height('100%').borderRadius(20).backgroundImage($r('app.media.bg')).backgroundImageSize(ImageSize.FILL)// 内容层Column() {Text('每日签到,开启健康每一天').fontSize(18).fontColor('#ff3952a5')// 星期签到Row() {Flex({ direction: FlexDirection.Row }) {ForEach(['一', '二', '三', '四', '五', '六', '日'], (item: string) => {Column({ space: 10 }) {Text(`周${item}`)Image($r('app.media.icon_true_green')).width(27).aspectRatio(1)}.flexGrow(1)})}}.padding(10)Button('点击签到').margin(10)}.width('100%').height('100%').padding(12).borderRadius(20).backgroundImage($r('app.media.home_meiriqiaodao2'))// 装饰图片Image($r('app.media.home_v')).width(75).height(95).position({ x: '75%', y: -55 })}}.height('30%')// 4. 生活提醒Column() {Text('生活提醒').fontSize(21).fontWeight(FontWeight.Bold)CardComp({gradientStart: '#FFEDE4',gradientEnd: '#FFFFFF',paddingValue: 5,wid: '100%',params: lifeReminder,contentBuilder: itemContent})}// 5. 体检预约Column() {Text('体检预约').fontSize(21).fontWeight(FontWeight.Bold)CardComp({gradientStart: '#CCF2FF',gradientEnd: '#FFFFFF',paddingValue: 5,wid: '100%',params: checkUp,contentBuilder: itemContent})}.onClick(() => {this.pathStack.pushPathByName('HealthCheckPage', null)})}.padding($r('app.float.global_padding_or_margin'))}.width('100%').height('100%').backgroundImage($r("app.media.bg")).backgroundImageSize(ImageSize.Cover)}}
}// 导航卡片内容
@Builder
function navItemContent(param: NavItem) {Row({ space: 2 }) {Image(param.img).width(55).height(55)Column({ space: 5 }) {Text(param.title).fontSize(21).fontWeight(FontWeight.Bold)Text(param.subtitle).fontSize(14).fontColor(Color.Gray)}.alignItems(HorizontalAlign.Start)}
}// 简单卡片内容
@Builder
function itemContent(param: NavItem) {Row({ space: 2 }) {Image(param.img).width(44).height(44)Text(param.title).fontSize(14)}.width('100%')
}
知识点详解:
5.3.1 Grid 网格布局
Grid() {ForEach(items, (item) => {GridItem() {// 网格项内容}})
}
.rowsTemplate('1fr 1fr') // 2行,平分高度
.columnsTemplate('1fr 1fr 1fr') // 3列,平分宽度
.rowsGap(10) // 行间距
.columnsGap(10) // 列间距
模板语法:
1fr
- 弹性单位(平分剩余空间)100px
- 固定像素1fr 2fr
- 按比例分配(1:2)
5.3.2 Flex 弹性布局
Flex({ direction: FlexDirection.Row, // 方向justifyContent: FlexAlign.SpaceBetween, // 主轴对齐alignItems: ItemAlign.Center // 交叉轴对齐
}) {// 子组件
}
FlexDirection 枚举:
Row
- 水平(从左到右)RowReverse
- 水平(从右到左)Column
- 垂直(从上到下)ColumnReverse
- 垂直(从下到上)
5.3.3 flexGrow 属性
Column() {// 内容
}
.flexGrow(1) // 占据剩余空间
5.3.4 position 绝对定位
Image($r('app.media.icon')).width(75).height(95).position({ x: '75%', y: -55 }) // 绝对定位
第六章:状态管理
6.1 组件内部状态:@State
@Component
struct Counter {@State count: number = 0 // 状态变量build() {Column() {Text(`计数: ${this.count}`)Button('增加').onClick(() => {this.count++ // 修改状态,自动刷新UI})}}
}
@State 特点:
- ✅ 状态改变自动刷新UI
- ✅ 仅在当前组件内有效
- ✅ 支持基本类型和对象类型
6.2 父子组件通信:@Prop 和 @Link
6.2.1 @Prop - 单向传递
// 父组件
@Entry
@Component
struct Parent {@State message: string = 'Hello'build() {Column() {Child({ msg: this.message })}}
}// 子组件
@Component
struct Child {@Prop msg: string // 接收父组件数据build() {Text(this.msg)}
}
@Prop 特点:
- ✅ 单向数据流:父 → 子
- ✅ 子组件修改不影响父组件
- ✅ 适合配置型数据
6.2.2 @Link - 双向绑定
// 父组件
@Entry
@Component
struct Parent {@State count: number = 0build() {Column() {Text(`父组件: ${this.count}`)Child({ count: $count }) // 使用 $ 传递引用}}
}// 子组件
@Component
struct Child {@Link count: number // 双向绑定build() {Column() {Text(`子组件: ${this.count}`)Button('增加').onClick(() => {this.count++ // 修改会同步到父组件})}}
}
@Link 特点:
- ✅ 双向数据流:父 ↔ 子
- ✅ 子组件修改同步父组件
- ✅ 使用
$
传递引用
6.3 全局状态:AppStorage
// 设置全局状态
AppStorage.setOrCreate('CurrentTabIndex', 0)
AppStorage.setOrCreate('IsLoggedIn', false)
AppStorage.setOrCreate('pathStack', this.pathStack)// 获取全局状态
let tabIndex = AppStorage.get<number>('CurrentTabIndex')
let pathStack = AppStorage.get("pathStack") as NavPathStack// 使用 @StorageLink 装饰器
@Component
struct MyComponent {@StorageLink('CurrentTabIndex') tabIndex: number = 0build() {Text(`当前标签: ${this.tabIndex}`)}
}
AppStorage 使用场景:
- ✅ 跨页面共享数据
- ✅ 全局配置信息
- ✅ 用户登录状态
- ✅ 路由栈共享
6.4 状态管理最佳实践
// ✅ 推荐:合理使用状态
@State isLoading: boolean = false
@State userInfo: UserInfo | null = null
@State errorMsg: string = ''// ❌ 避免:不必要的状态
// 可以通过计算得到的值不需要状态
@State fullName: string = '' // 不推荐// ✅ 推荐:使用计算属性
get fullName(): string {return this.firstName + ' ' + this.lastName
}
第七章:路由导航
7.1 router 路由跳转
7.1.1 基本用法
import { router } from '@kit.ArkUI'// 1. 跳转到新页面(保留当前页面)
router.pushUrl({url: 'pages/LoginPage'
})// 2. 跳转并传递参数
router.pushUrl({url: 'pages/DetailPage',params: {id: '123',name: '张三'}
})// 3. 替换当前页面(不保留历史)
router.replaceUrl({url: 'pages/HomePage'
})// 4. 返回上一页
router.back()// 5. 返回到指定页面
router.back({url: 'pages/Index'
})
7.1.2 接收路由参数
import { router } from '@kit.ArkUI'@Entry
@Component
struct DetailPage {@State id: string = ''@State name: string = ''aboutToAppear() {// 获取路由参数const params = router.getParams() as Record<string, string>this.id = params['id']this.name = params['name']}build() {Column() {Text(`ID: ${this.id}`)Text(`姓名: ${this.name}`)}}
}
7.2 Navigation 路由栈
7.2.1 配置路由
route_map.json
:
{"routerMap": [{"name": "HealthCheckPage","pageSourceFile": "src/main/ets/pages/home/HealthCheckPage.ets","buildFunction": "HealthCheckPageBuilder"},{"name": "HospitalRankingPage","pageSourceFile": "src/main/ets/pages/home/HospitalRankingPage.ets","buildFunction": "HospitalRankingPageBuilder"}]
}
7.2.2 创建路由栈
@Entry
@Component
struct Index {pathStack: NavPathStack = new NavPathStack()aboutToAppear(): void {// 存储到全局,供其他组件使用AppStorage.setOrCreate("pathStack", this.pathStack)}build() {Navigation(this.pathStack) {// 页面内容}}
}
7.2.3 使用路由栈跳转
@Component
export default struct HomeComp {// 获取全局路由栈pathStack: NavPathStack = AppStorage.get("pathStack") as NavPathStackbuild() {NavDestination() {Column() {Button('跳转到健康检测').onClick(() => {// 跳转到命名路由this.pathStack.pushPathByName('HealthCheckPage', null)})Button('跳转并传参').onClick(() => {this.pathStack.pushPathByName('DetailPage', {id: '123',name: '测试'})})Button('返回').onClick(() => {this.pathStack.pop()})Button('清空路由栈').onClick(() => {this.pathStack.clear()})}}}
}
7.2.4 目标页面配置
// 页面文件:HealthCheckPage.ets@Builder
export function HealthCheckPageBuilder() {HealthCheckPage()
}@Component
struct HealthCheckPage {// 接收路由参数@State params: Record<string, Object> = {}aboutToAppear() {// 获取路由参数的方式// this.params = ...}build() {NavDestination() {Column() {Text('健康检测页面')}}.title('健康检测') // 设置页面标题}
}
7.3 路由对比
特性 | router | Navigation |
---|---|---|
使用场景 | 页面级跳转 | 组件级导航 |
是否需要配置 | 需要在 main_pages.json | 需要在 route_map.json |
传参方式 | params 对象 | pushPathByName 参数 |
历史管理 | 全局页面栈 | 组件内路由栈 |
适用场景 | 独立页面跳转 | 多层级导航 |
第八章:数据模型
8.1 定义数据模型
文件位置: entry/src/main/ets/pages/model/NavCard.ets
// 导航卡片数据模型
class NavItem {img: ResourceStr // 图片资源title: string // 标题subtitle?: string // 副标题(可选)url?: string // 跳转路径(可选)constructor(img: ResourceStr,title: string,subtitle?: string,url?: string) {this.img = imgthis.title = titlethis.subtitle = subtitlethis.url = url}
}// 创建数据实例
const navItems: NavItem[] = [new NavItem($r('app.media.img_jiankangjiance'),'健康检测','守护生活每一刻','HealthCheckPage'),new NavItem($r('app.media.img_yaopinshouce'),'药品手册','明智用药',''),new NavItem($r('app.media.img_yiyuanpaihang'),'医院排行','选择优质资源','HospitalRankingPage')
]// 单个数据实例
const lifeReminder: NavItem = new NavItem($r('app.media.home_life_reminder_svg'),'今天需要吃降压药两粒哟'
)// 导出供其他组件使用
export { NavItem, navItems, lifeReminder }
8.2 使用数据模型
import { NavItem, navItems } from '../model/NavCard'@Component
export default struct HomeComp {build() {Column() {// 使用数据模型ForEach(navItems, (item: NavItem, index) => {Row() {Image(item.img)Column() {Text(item.title)Text(item.subtitle)}}})}}
}
8.3 接口 vs 类
// 方式1: 使用 interface(接口)
interface NavItemInterface {img: ResourceStrtitle: stringsubtitle?: string
}const item1: NavItemInterface = {img: $r('app.media.icon'),title: '标题'
}// 方式2: 使用 class(类)
class NavItemClass {img: ResourceStrtitle: stringsubtitle?: stringconstructor(img: ResourceStr, title: string, subtitle?: string) {this.img = imgthis.title = titlethis.subtitle = subtitle}// 可以添加方法getFullTitle(): string {return this.subtitle ? `${this.title} - ${this.subtitle}` : this.title}
}const item2 = new NavItemClass($r('app.media.icon'), '标题')
选择建议:
- ✅ 使用
interface
- 纯数据结构 - ✅ 使用
class
- 需要方法或构造函数
第九章:图表组件
9.1 集成第三方图表库
安装依赖:
ohpm install @ohos/mpchart
oh-package.json5
:
{"dependencies": {"@ohos/mpchart": "^2.0.0"}
}
9.2 折线图实现
文件位置: entry/src/main/ets/pages/view/LineCharts.ets
import {JArrayList,XAxis,XAxisPosition,YAxis,YAxisLabelPosition,LineDataSet,LineData,Mode,LineChart,LineChartModel,LimitLine,LimitLabelPosition,EntryOhos,IAxisValueFormatter
} from '@ohos/mpchart'// 1. 自定义X轴格式化器
class WeekFormatter implements IAxisValueFormatter {private readonly weeks = ['周一', '周二', '周三', '周四', '周五', '周六', '周日']getFormattedValue(value: number): string {return this.weeks[Math.round(value) % 7] || ''}
}@Component
export default struct LineCharts {// 2. 初始化模型private model: LineChartModel = new LineChartModel()private dataSet: LineDataSet = new LineDataSet(new JArrayList<EntryOhos>(), "高压")private dataSet2: LineDataSet = new LineDataSet(new JArrayList<EntryOhos>(), "低压")// 阈值线private limitLine1: LimitLine = new LimitLine(120, '危险阈值')private limitLine2: LimitLine = new LimitLine(50, '警戒阈值')// 3. 外部传入的数据@Prop data1: Array<number | null> = [600, 200, 1000, 620, 790, 410, 175]@Prop data2: Array<number | null> = [400, 150, 900, 200, 210, 300, 405]@Prop wid: number | string = '300'@Prop hei: number | string = '200'aboutToAppear() {// 4. 基础配置this.model.getDescription()?.setEnabled(false) // 禁用描述this.model.setDragEnabled(true) // 启用拖拽// 5. 配置坐标轴this.configureAxis()// 6. 配置阈值线this.configureLimitLines()// 7. 绑定数据this.model.setData(this.generateMockData())this.model.setVisibleXRangeMaximum(7) // 显示7个点}// 8. 坐标轴配置private configureAxis() {// X轴配置const xAxis = this.model.getXAxis()xAxis?.setPosition(XAxisPosition.BOTTOM) // 位置:底部xAxis?.setGranularity(1) // 最小间隔xAxis?.setLabelCount(7, true) // 标签数量xAxis?.setValueFormatter(new WeekFormatter()) // 自定义格式化// 左Y轴配置const leftAxis = this.model.getAxisLeft()leftAxis?.setAxisMinimum(0) // 最小值leftAxis?.setAxisMaximum(1000) // 最大值leftAxis?.setPosition(YAxisLabelPosition.OUTSIDE_CHART)leftAxis?.setSpaceTop(15) // 顶部留白// 右Y轴禁用this.model.getAxisRight()?.setEnabled(false)}// 9. 阈值线配置private configureLimitLines() {// 危险阈值线(红色虚线)this.limitLine1.setLineWidth(3)this.limitLine1.enableDashedLine(10, 10, 0) // 虚线样式this.limitLine1.setTextSize(12)this.limitLine1.setLabelPosition(LimitLabelPosition.RIGHT_TOP)this.limitLine1.setLineColor("#FF3D71")// 警戒阈值线(橙色虚线)this.limitLine2.setLineWidth(3)this.limitLine2.enableDashedLine(10, 10, 0)this.limitLine2.setTextSize(12)this.limitLine2.setLabelPosition(LimitLabelPosition.RIGHT_BOTTOM)this.limitLine2.setLineColor("#FFAA33")// 添加到Y轴this.model.getAxisLeft()?.addLimitLine(this.limitLine1)this.model.getAxisLeft()?.addLimitLine(this.limitLine2)}// 10. 生成数据private generateMockData(): LineData {// 第一条线数据const values = new JArrayList<EntryOhos>()for (let i = 0; i < this.data1.length; i++) {values.add(new EntryOhos(i, this.data1[i]))}// 第二条线数据const values2 = new JArrayList<EntryOhos>()for (let i = 0; i < this.data2.length; i++) {values2.add(new EntryOhos(i, this.data2[i]))}// 设置数据this.dataSet.setValues(values)this.dataSet2.setValues(values2)// 数据集1样式this.dataSet.setMode(Mode.CUBIC_BEZIER) // 贝塞尔曲线this.dataSet.setColorByColor(Color.Blue) // 线条颜色this.dataSet.setLineWidth(2) // 线宽this.dataSet.setDrawCircles(false) // 不显示圆点this.dataSet.setDrawFilled(true) // 填充渐变// 数据集2样式this.dataSet2.setColorByColor(Color.Green)this.dataSet2.setDrawCircles(false)this.dataSet2.setMode(Mode.CUBIC_BEZIER)// 返回数据return new LineData([this.dataSet, this.dataSet2])}// 11. 页面构建build() {Column() {LineChart({ model: this.model }).width(this.wid).height(this.hei).backgroundColor("#F5F7FA").onAppear(() => {// 启动动画this.model.animateXY(1500, 1500)})}}
}
9.3 使用图表组件
import LineCharts from '../view/LineCharts'@Component
struct DataPage {@State bpHighData: number[] = [120, 125, 118, 130, 115, 122, 119]@State bpLowData: number[] = [80, 85, 78, 90, 75, 82, 79]build() {Column() {Text('血压趋势').fontSize(20).fontWeight(FontWeight.Bold)LineCharts({wid: '100%',hei: 300,data1: this.bpHighData,data2: this.bpLowData})}}
}
知识点总结:
- ✅ 使用第三方库需要先安装依赖
- ✅ 图表配置包括:坐标轴、样式、数据
- ✅ 可以自定义格式化器
- ✅ 支持多条数据线
第十章:最佳实践
10.1 组件设计原则
10.1.1 单一职责
// ❌ 不好:一个组件做太多事
@Component
struct BadComponent {build() {Column() {// 导航栏Row() { }// 内容区Column() { }// 底部栏Row() { }}}
}// ✅ 好:拆分成多个组件
@Component
struct GoodComponent {build() {Column() {TopNav()ContentArea()BottomBar()}}
}
10.1.2 可复用性
// ✅ 好:可配置的通用组件
@Component
export default struct CustomButton {@Prop text: string@Prop bgColor: string = '#4D91FF'@Prop fontSize: number = 16onClickHandler?: () => voidbuild() {Button(this.text).backgroundColor(this.bgColor).fontSize(this.fontSize).onClick(() => {this.onClickHandler?.()})}
}
10.2 性能优化
10.2.1 减少不必要的渲染
// ❌ 不好:频繁创建对象
build() {Column() {ForEach(this.items, (item) => {Row() {Text(item.name).fontSize(this.getSize()) // 每次都调用}})}
}// ✅ 好:缓存计算结果
private cachedSize: number = 16aboutToAppear() {this.cachedSize = this.getSize()
}build() {Column() {ForEach(this.items, (item) => {Row() {Text(item.name).fontSize(this.cachedSize) // 使用缓存}})}
}
10.2.2 LazyForEach 懒加载
class BasicDataSource implements IDataSource {private listeners: DataChangeListener[] = []public totalCount(): number {return 0}public getData(index: number): Object {return undefined}registerDataChangeListener(listener: DataChangeListener): void {if (this.listeners.indexOf(listener) < 0) {this.listeners.push(listener)}}unregisterDataChangeListener(listener: DataChangeListener): void {const pos = this.listeners.indexOf(listener)if (pos >= 0) {this.listeners.splice(pos, 1)}}
}@Component
struct LongList {private dataSource: BasicDataSource = new BasicDataSource()build() {List() {LazyForEach(this.dataSource, (item: Object) => {ListItem() {Text(item.toString())}}, item => item.id)}}
}
10.3 代码组织
10.3.1 文件命名
✅ 推荐命名规范:
- 组件文件: PascalCase (HomeComp.ets, CardComp.ets)
- 页面文件: PascalCase (LoginPage.ets, Index.ets)
- 工具类: camelCase (httpUtil.ets, dateUtil.ets)
- 模型类: PascalCase (NavCard.ets, UserInfo.ets)
10.3.2 目录结构
pages/
├── comp/ # 公共组件
├── model/ # 数据模型
├── view/ # 视图组件
├── utils/ # 工具函数
├── home/ # 首页模块
├── profile/ # 个人中心模块
└── common/ # 通用样式
10.4 样式管理
10.4.1 使用 @Extend 统一样式
// 定义全局样式扩展
@Extend(Text)
function primaryText() {.fontSize(16).fontColor('#333333').fontWeight(FontWeight.Normal)
}@Extend(Text)
function titleText() {.fontSize(24).fontColor('#000000').fontWeight(FontWeight.Bold)
}// 使用
build() {Column() {Text('标题').titleText()Text('内容').primaryText()}
}
10.4.2 使用资源文件
resources/base/element/color.json
:
{"color": [{"name": "primary_color","value": "#4D91FF"},{"name": "text_primary","value": "#333333"}]
}
resources/base/element/float.json
:
{"float": [{"name": "global_padding_or_margin","value": "16vp"},{"name": "title_font_size","value": "24fp"}]
}
使用资源:
Text('标题').fontSize($r('app.float.title_font_size')).fontColor($r('app.color.text_primary')).padding($r('app.float.global_padding_or_margin'))
10.5 错误处理
// ✅ 好:完善的错误处理
private jumpToPage(url: string) {try {router.pushUrl({ url: url })} catch (error) {console.error(`路由跳转失败: ${error.code} - ${error.message}`)// 显示错误提示promptAction.showToast({message: '页面跳转失败,请重试'})}
}
第十一章:常见问题
11.1 装饰器相关
Q1: @State 和 @Prop 的区别?
// @State: 组件内部状态
@State count: number = 0
// ✅ 修改会刷新UI
// ✅ 只在当前组件有效// @Prop: 父组件传入的属性
@Prop count: number
// ✅ 父组件修改会同步到子组件
// ✅ 子组件修改不影响父组件
Q2: 什么时候使用 @Link?
// 需要父子组件双向同步时使用
@Component
struct Child {@Link count: number // 双向绑定build() {Button(`子组件: ${this.count}`).onClick(() => {this.count++ // 修改会同步到父组件})}
}// 父组件传递时使用 $
Child({ count: $count })
11.2 布局相关
Q3: 如何实现水平居中?
// 方法1: Column + alignItems
Column() {Text('居中文本')
}
.alignItems(HorizontalAlign.Center)// 方法2: Row + justifyContent
Row() {Text('居中文本')
}
.justifyContent(FlexAlign.Center)
Q4: 如何实现垂直居中?
// Column + justifyContent
Column() {Text('居中文本')
}
.justifyContent(FlexAlign.Center)
.height('100%')
11.3 路由相关
Q5: router 和 Navigation 有什么区别?
特性 | router | Navigation |
---|---|---|
跳转范围 | 页面级 | 组件级 |
配置文件 | main_pages.json | route_map.json |
使用场景 | 独立页面 | 多层导航 |
返回栈 | 全局 | 组件内 |
Q6: 如何传递复杂对象?
// router 方式
router.pushUrl({url: 'pages/DetailPage',params: {user: JSON.stringify({ // 转为JSON字符串id: 1,name: '张三'})}
})// 接收时解析
const params = router.getParams()
const user = JSON.parse(params['user'])
11.4 性能相关
Q7: ForEach 和 LazyForEach 的区别?
// ForEach: 一次性渲染所有
ForEach(this.items, (item) => {ListItem() { Text(item) }
})
// ✅ 适合少量数据(< 100)
// ❌ 大量数据会卡顿// LazyForEach: 按需渲染
LazyForEach(this.dataSource, (item) => {ListItem() { Text(item) }
})
// ✅ 适合大量数据
// ✅ 性能更好
11.5 样式相关
Q8: 如何设置圆角?
Column() { }.borderRadius(12) // 所有角.borderRadius({ // 单独设置topLeft: 12,topRight: 12,bottomLeft: 0,bottomRight: 0})
Q9: 如何设置阴影?
Column() { }.shadow({radius: 10,color: '#00000033',offsetX: 0,offsetY: 2})
总结
学习路径建议
第1周: 熟悉项目结构,理解启动流程↓
第2周: 学习组件开发,掌握装饰器↓
第3周: 深入状态管理和路由导航↓
第4周: 数据模型和图表组件↓
第5周: 最佳实践和性能优化
实践建议
- 动手实践: 运行项目,修改代码,观察效果
- 阅读文档: 查阅 HarmonyOS 官方文档
- 调试技巧: 使用 console.log 和 DevEco 调试工具
- 代码对比: 对比本项目与官方示例
- 循序渐进: 从简单组件开始,逐步深入
进阶方向
- 🚀 网络请求和 HTTP 客户端
- 🗄️ 本地数据库(Preferences、RelationalStore)
- 📱 系统能力调用(相机、位置、通知)
- 🎨 动画和转场效果
- 🔐 安全和加密
- 📦 应用打包和发布
祝学习愉快!遇到问题随时查阅本文档。 🎉