鸿蒙 ArkTS 实战:从零开发体检报告 App
鸿蒙 ArkTS 实战:从零开发体检报告 App
本文将以一个真实的鸿蒙(HarmonyOS)应用项目为案例,从需求分析、架构设计到核心代码实现,完整拆解如何使用 ArkTS 语言开发一款"体检报告查看"应用。读者将跟随文章一步步搭建出包含历史报告列表、关键指标趋势图、报告详情、个人中心等多页面的完整应用,并理解鸿蒙应用开发中的核心概念、UI 组件体系以及路由跳转机制。
运行截图




一、前言:为什么选择鸿蒙 ArkTS?
随着 HarmonyOS 生态的快速扩张,越来越多的开发者开始关注这一全新的分布式操作系统。HarmonyOS 不仅在手机、平板上运行,还覆盖了智慧屏、手表、车机、智能家居等众多终端。这种"一次开发,多端部署"的能力,是它最具想象力的优势之一。
ArkTS 作为鸿蒙主推的应用开发语言,在保留 TypeScript 风格的基础上,做了三个关键强化:
第一,强化了静态类型系统。TypeScript 本身是"可选类型"的超集,但 ArkTS 在编译器层面做了更严格的约束,比如要求所有变量必须有明确类型、禁止隐式 any、禁止动态属性访问等。这样做的好处是:编译器能在开发期就捕获大量错误,运行时崩溃率显著降低。
第二,深度优化了声明式 UI。ArkUI 提供的组件都采用声明式写法,开发者只需要描述"页面应该长什么样",而无需关心"如何更新 DOM"。当状态变化时,框架会自动重渲染相关部分,让代码更接近自然语言。
第三,原生支持 HarmonyOS 的分布式能力。比如"流转"(在多个设备间无缝迁移应用状态)、“原子化服务”(免安装运行)、"统一设备发现"等,都能在 ArkTS 中以简单 API 直接调用。
本项目"体检报告查看 App"聚焦在一个相对小而美的垂直场景:用户希望随时查看自己过往的体检报告,对比多次体检之间关键指标的变化趋势,并获得直观的健康提示。这类应用看似简单,但要做到体验流畅、视觉精致,仍然需要处理不少细节:
- 历史报告应当以列表形式清晰呈现,且支持点击进入详情;
- 关键指标应当用趋势图直观展示,并支持点击查看大图与历史数值;
- 页面间跳转需要借助鸿蒙的路由机制,并正确传递参数;
- 底部 Tab 导航是绝大多数 App 的标配,能够让用户在不同功能模块间快速切换;
- 页面状态共享需要在不引入复杂状态管理库的情况下,让多个页面读到同一份数据。
本文将围绕以上几个核心点展开,从零开始构建这款应用。读完本文后,你不仅会得到一份可运行的完整代码,更会对 ArkTS 的开发模式、组件化思想以及鸿蒙应用的工程结构有一个系统的认识。
无论你是初次接触鸿蒙生态的前端工程师,还是希望把现有 Web/小程序项目迁移到鸿蒙的开发者,相信都能从本文中有所收获。
二、项目需求与功能拆解
2.1 业务需求
体检报告查看 App 主要面向需要长期管理自己健康数据的用户。在中国,成年人每年至少会进行一次体检,少则 5 年、10 年累积下来就是 5-10 份厚厚的纸质报告。但这些报告往往被束之高阁,真正能被"二次利用"的少之又少。一款好的体检 App,应当让用户用最低的成本完成以下几件事:
- 历次体检报告列表:以时间倒序展示用户所有的体检记录,每条记录展示日期、医院、简要结论与关键指标摘要。
- 关键指标趋势图:在首页以网格形式展示血压、体重、心率、血糖、BMI 等关键指标的变化趋势,每张图都是一个 Canvas 自绘的折线图。
- 报告详情页:点击某条报告后进入详情,可以查看完整的体检机构信息、体检结论以及所有指标的详细数值与参考范围。
- 指标详情页:点击首页的某张趋势图,进入该指标的独立详情页,查看大尺寸趋势图、最新/最高/最低/平均值,以及历次数值列表。
- 个人中心:展示用户基本信息、健康摘要(报告数/指标数/异常项数)、常用功能入口(我的报告、健康趋势、健康目标、设置等)。
- 底部 Tab 导航:报告、趋势、我的三个 Tab 自由切换。
2.2 功能拆解
将上述需求拆解为可实现的功能模块:
- 数据模型层:定义
HealthReport(单次体检报告)、MetricItem(单个指标)、MetricSnapshot(单个指标的多期数据)、状态判断函数getMetricStatus。这一层是整个应用的基石,所有数据都从这里流出。 - 通用组件层:自绘折线图组件
MetricChart,可复用于首页小图与详情页大图。组件化的好处是:未来如果想替换成另一种图表样式,只需改这一处即可。 - 页面层:
Index.ets(主页面,承载 Tabs)ReportDetail.ets(报告详情)MetricDetail.ets(指标详情)Profile.ets(个人中心)
- 路由与导航:通过
@ohos.router实现页面跳转与参数传递,并在main_pages.json中注册所有页面。 - UI 资源:中文字符串资源,方便后续做国际化;颜色、字号等集中在少量位置声明。
2.3 非功能性需求
除了业务功能外,我们也应当考虑一些非功能性需求:
- 性能:首页渲染应当流畅,Canvas 重绘频率受控,不影响主线程。
- 可维护性:所有 UI 文本应集中管理,避免散落各处的"魔法字符串"。
- 可扩展性:未来接入后端 API 时,只需替换
SAMPLE_REPORTS与KEY_METRICS两个常量即可,组件层不需要改动。 - 可访问性:文字大小、颜色对比度应当满足一般可读性要求;状态色用绿/红,而非仅用红。
三、开发环境与工程结构
3.1 开发环境
- DevEco Studio:鸿蒙官方 IDE,建议使用最新稳定版(如 5.0.3 及以上)。它基于 IntelliJ IDEA 平台,对 TS/ArkTS 语法、ArkUI 组件、模拟器、调试器等都有良好的支持。
- HarmonyOS SDK:API 12 及以上(兼容 API 9 之后的大部分能力)。建议在 SDK Manager 中下载最新的
Full SDK与对应版本的模拟器镜像。 - ArkTS 编译器:随 DevEco Studio 一起安装,编译阶段会做严格的类型检查。
- 运行设备:HarmonyOS 5.0 及以上的真机,或使用远程模拟器 / 本地模拟器(推荐使用真机调试,能更准确地反映 Canvas 性能)。
3.2 工程目录
ArkTSReport/
├── AppScope/ # 应用级资源
│ ├── app.json5
│ └── resources/base/element/
│ └── string.json
├── entry/ # 主模块
│ ├── src/main/ets/
│ │ ├── entryability/ # 入口能力(UIAbility)
│ │ ├── entrybackupability/ # 备份能力
│ │ ├── pages/ # 页面
│ │ │ ├── Index.ets # 主页面(Tabs)
│ │ │ ├── ReportDetail.ets # 报告详情
│ │ │ ├── MetricDetail.ets # 指标详情
│ │ │ └── Profile.ets # 个人中心
│ │ ├── components/ # 通用组件
│ │ │ └── MetricChart.ets # 自绘折线图
│ │ └── model/ # 数据模型
│ │ └── HealthReport.ets # 健康报告数据
│ └── src/main/resources/ # 资源
│ ├── base/element/ # 颜色、字符串、浮点
│ ├── base/media/ # 图片
│ └── base/profile/ # profile(页面注册)
│ └── main_pages.json
└── ...
整个工程结构清晰:模型、组件、页面、路由、资源各司其职,便于后期维护与扩展。
3.3 关键配置文件
AppScope/app.json5:应用级配置,包含appName、versionCode、versionName等。entry/src/main/module.json5:模块级配置,包含mainElement、pages、abilities等。entry/src/main/resources/base/profile/main_pages.json:页面路由表,所有页面必须在此注册。entry/src/main/resources/base/element/string.json:字符串资源。
对于本项目,最关键的改动是 main_pages.json:每新增一个页面,都要在此处添加。
四、数据模型设计
4.1 为什么先设计数据模型?
良好的数据模型是应用稳定运行的基础。在 ArkTS 中,由于类型系统是静态的,预先定义好接口可以让编译器帮助我们捕获错误,提升代码可读性。设想一下,如果整页代码都在直接操作 any 类型对象,那将是一场灾难:IDE 没法补全,重构时无法批量改字段名,运行时容易出现拼写错误。
我们定义了如下几个核心接口:
// 单个指标
export interface MetricItem {
name: string; // 指标名称:身高、体重、血压...
value: number; // 当前数值
unit: string; // 单位:cm、kg、mmHg...
normalLow: number; // 正常下限
normalHigh: number; // 正常上限
}
// 单个指标的多期数据(用于趋势图)
export interface MetricSnapshot {
name: string;
unit: string;
values: number[]; // 按时间顺序
normalLow: number;
normalHigh: number;
}
// 单次体检报告
export interface HealthReport {
date: string; // 体检日期
hospital: string; // 体检机构
summary: string; // 体检结论
metrics: MetricItem[]; // 各项指标
}
设计上有几个要点:
- 统一单位:所有数值都使用国际标准单位(kg、mmHg、mmol/L 等),避免展示时混淆。
- normalLow / normalHigh:通过参考范围对每个指标做"正常/偏高/偏低"的状态判断。即使不显示参考值,前端也能算出状态。
- 分离多期数据:单次报告用
MetricItem[]表达全部指标;趋势图用MetricSnapshot表达某个指标的多次数据,这样两套数据结构互不干扰。前者关心"一次体检有哪些指标",后者关心"某个指标的历史走势"。 - 字符串日期:日期用 ISO 格式(
YYYY-MM-DD)的字符串存储,便于排序与展示。
4.2 状态判断函数
export function getMetricStatus(value: number, low: number, high: number): string {
if (low === 0 && high === 0) {
return '正常';
}
if (value < low) {
return '偏低';
}
if (value > high) {
return '偏高';
}
return '正常';
}
该函数非常小巧,但被广泛使用:报告卡上指标的颜色、详情页的状态文字、趋势图角标、首页异常项统计都依赖它。把它抽成纯函数的好处是:测试简单、易于复用、未来加新规则(比如"严重偏高")时只改这一处。
需要注意的是:normalLow === 0 && normalHigh === 0 表示"该指标没有标准参考范围"(比如身高、体重),这种指标我们直接判定为"正常"。
4.3 示例数据
为了让应用一启动就能看到内容,我们准备了一组模拟数据:5 次体检(2023-06 至 2025-12)、6 个关键指标。每次体检的指标都合理变化,比如血压从偏高逐渐改善,体重先升后降,呈现出真实健康管理的"曲线感"。
export const SAMPLE_REPORTS: HealthReport[] = [ /* 5 条历史报告 */ ];
export const KEY_METRICS: MetricSnapshot[] = [ /* 6 个指标的多次数据 */ ];
实际项目中,这些数据应当来自后端 API 或本地数据库。本项目为演示用,直接在模型文件中以常量形式提供。未来接入真实数据时,可以将这两个常量替换为异步加载的 Promise,并在 aboutToAppear 中赋值给 @State 变量。
4.4 关于"指标"的思考
在设计指标时,我们刻意把"身高""体重"这种没有参考范围的指标也归入同一模型,通过 normalLow = 0, normalHigh = 0 来标记。这样做有两个好处:
- 代码统一:UI 不需要为"有参考值"和"无参考值"分别写渲染分支,只需根据
getMetricStatus返回值判断要不要显示状态文字。 - 未来扩展:如果某天医院新增了身高的"标准体重对照表",只需修改
getMetricStatus函数即可,不需要改数据结构。
五、自绘折线图组件:MetricChart
5.1 为什么不用第三方图表库?
鸿蒙生态中有不少优秀的图表库(如 MPChartHarmony、VChart 等),但本项目刻意选择自绘 Canvas 折线图,原因有三:
- 零依赖:无需安装额外组件,纯 API 实现,减少包体积。
- 可定制:可以根据设计需要调整颜色、网格、标注等细节,第三方库往往有大量配置项但仍不够灵活。
- 教学价值:自己实现图表能深入理解 Canvas 渲染管线和坐标变换,对其他可视化场景也有帮助。
5.2 核心思路
折线图本质上就是把一组数值映射到画布的像素坐标,然后用 moveTo / lineTo 画出来。具体步骤:
- 确定画布尺寸:通过
onAreaChange回调拿到画布真实宽高,避免硬编码导致 Grid 拉伸时图表变形。 - 计算数据范围:找出
min和max,并向上下各扩 10%,让曲线不要顶到边框。 - 绘制网格:用浅灰色横线 + 数值文字,给用户视觉刻度参考。
- 绘制折线:用蓝色实线连接所有数据点。
- 绘制数据点:在每个点位置画一个实心小圆,强化数据位置感。
- 绘制数值:在大图版本中,将每个点的数值标在点上方,方便用户读数。
5.3 关键代码
@Component
export struct MetricChart {
@Prop snapshot: MetricSnapshot;
private context: CanvasRenderingContext2D = new CanvasRenderingContext2D(
new RenderingContextSettings(true)
);
@State canvasWidth: number = 160;
@State canvasHeight: number = 110;
build() {
Column() {
Row() {
Text(this.snapshot.name).fontSize(15).fontWeight(FontWeight.Medium);
Blank();
Text(this.latestValue() + ' ' + this.snapshot.unit).fontSize(13).fontColor('#666666');
}
.width('100%')
.padding({ left: 10, right: 10, top: 8, bottom: 4 });
Canvas(this.context)
.width('100%')
.height(110)
.onReady(() => { this.drawChart(); })
.onAreaChange((_oldVal: Area, newVal: Area) => {
this.canvasWidth = newVal.width as number;
this.canvasHeight = newVal.height as number;
this.drawChart();
});
Text(this.statusText())
.fontSize(12)
.fontColor(this.statusColor())
.margin({ top: 4, bottom: 8 });
}
.width('100%')
.height(180)
.backgroundColor('#FFFFFF')
.borderRadius(8);
}
}
几个关键点:
@Prop snapshot:从父组件接收的指标数据。@Prop表示单向同步,父组件更新后子组件会跟着重渲染。@State canvasWidth/Height:必须用@State修饰,因为它们的变化需要触发重绘。onReady:Canvas 首次挂载时调用一次。onAreaChange:画布尺寸变化时调用(Grid 拉伸、屏幕旋转等场景)。RenderingContextSettings(true):开启抗锯齿,让曲线和文字更平滑。
5.4 绘制函数
绘制函数 drawChart 负责把所有像素级操作集中起来:
private drawChart(): void {
const ctx = this.context;
const w = this.canvasWidth;
const h = this.canvasHeight;
if (w <= 0 || h <= 0) return;
const padL = 28, padR = 10, padT = 10, padB = 18;
ctx.clearRect(0, 0, w, h);
const values = this.snapshot.values;
if (values.length === 0) return;
// 1. 计算坐标范围
let minV = values[0], maxV = values[0];
for (let i = 0; i < values.length; i++) {
if (values[i] < minV) minV = values[i];
if (values[i] > maxV) maxV = values[i];
}
if (minV === maxV) { minV -= 1; maxV += 1; }
const range = maxV - minV;
minV -= range * 0.1; maxV += range * 0.1;
const chartW = w - padL - padR;
const chartH = h - padT - padB;
// 2. 网格
ctx.strokeStyle = '#EEEEEE';
ctx.lineWidth = 1;
for (let i = 0; i <= 4; i++) {
const y = padT + (chartH / 4) * i;
ctx.beginPath();
ctx.moveTo(padL, y);
ctx.lineTo(padL + chartW, y);
ctx.stroke();
}
// 3. 折线
ctx.strokeStyle = '#1E90FF';
ctx.lineWidth = 2;
ctx.beginPath();
const stepX = values.length > 1 ? chartW / (values.length - 1) : 0;
for (let i = 0; i < values.length; i++) {
const x = padL + stepX * i;
const y = padT + chartH - ((values[i] - minV) / (maxV - minV)) * chartH;
if (i === 0) ctx.moveTo(x, y);
else ctx.lineTo(x, y);
}
ctx.stroke();
// 4. 数据点
ctx.fillStyle = '#1E90FF';
for (let i = 0; i < values.length; i++) {
const x = padL + stepX * i;
const y = padT + chartH - ((values[i] - minV) / (maxV - minV)) * chartH;
ctx.beginPath();
ctx.arc(x, y, 3, 0, Math.PI * 2);
ctx.fill();
}
// 5. 极值标注
ctx.fillStyle = '#999999';
ctx.font = '10vp sans-serif';
ctx.textAlign = 'right';
ctx.fillText(maxV.toFixed(1), padL - 4, padT + 6);
ctx.fillText(minV.toFixed(1), padL - 4, padT + chartH + 2);
}
这段代码虽然不复杂,但包含了 Canvas 编程的几个关键技巧:
- 状态分离:每次绘制前
clearRect清除上次内容,否则折线会越来越粗。 - 比例映射:通过
padT + chartH - ((values[i] - minV) / (maxV - minV)) * chartH把数据值映射到画布 Y 坐标。这里的关键是 Y 轴在 Canvas 中是"从上到下"增长的,而我们想让"数值大"对应"位置高",所以要用chartH - ...翻转。 - 响应式重绘:通过
onAreaChange监听画布尺寸变化,保证 Grid 拉伸时图表不变形。 - 极值扩边:通过
minV -= range * 0.1给上下各留 10% 边距,让曲线不要顶到边框。
5.5 大图增强
在 MetricDetail 中,我们复用了同样的思路,但做了增强:
- 高度提升到 220vp;
- 添加 Y 轴刻度文字(最大值、最小值、3 个中间值);
- 添加 X 轴日期标签(取
MM-DD格式); - 在每个数据点上方标注数值;
- 提供"最新 / 最高 / 最低 / 平均"四个统计数字辅助查看。
这种"小图概览 + 大图详查"的设计,是健康类应用最常见的趋势图表范式。
5.6 性能与渲染时机
需要注意的是,Canvas 的绘制是"命令式"的:我们调用 stroke() / fill() 时,浏览器/鸿蒙渲染引擎才会真正去画。鸿蒙的 Canvas API 内部有缓冲机制,多次连续绘制会合并提交到 GPU 线程,因此性能损失很小。
但我们也要避免不必要的重绘。比如 onAreaChange 在父容器布局稳定前可能多次触发,每次都会重画一遍。生产环境里可以用 debounce 思路做防抖(ArkTS 内置 setTimeout 可以实现简单防抖),但本项目为了代码简洁没有引入。
六、主页面:Tabs + List + Grid 的复合布局
Index.ets 是整个应用的入口,也是最复杂的一页。它通过底部 Tabs 把"报告 / 趋势 / 我的"三大功能区组织起来,每个 Tab 内部又分别使用 List 与 Grid 展示数据。
6.1 整体结构
@Entry
@Component
struct Index {
@State currentTabIndex: number = 0;
@State reports: HealthReport[] = SAMPLE_REPORTS;
@State metrics: MetricSnapshot[] = KEY_METRICS;
@State selectedDate: string = '';
build() {
Column() {
this.HeaderBar();
Tabs({ barPosition: BarPosition.End, index: this.currentTabIndex }) {
TabContent() { this.ReportTab(); }.tabBar(this.TabBarBuilder('报告', 0));
TabContent() { this.TrendTab(); }.tabBar(this.TabBarBuilder('趋势', 1));
TabContent() { this.ProfileTab(); }.tabBar(this.TabBarBuilder('我的', 2));
}
.barMode(BarMode.Fixed)
.barHeight(56)
.onChange((index: number) => { this.currentTabIndex = index; });
}
.width('100%')
.height('100%')
.backgroundColor('#F5F6FA');
}
}
几个关键点:
@State:ArkTS 中响应式状态,状态变化会自动触发 UI 重新渲染。currentTabIndex变化时,顶部标题、Tabs 高亮等所有依赖它的 UI 都会自动更新。Tabs的barPosition: BarPosition.End:让 Tab 栏位于底部,更符合移动端用户习惯(参考微信、淘宝等主流 App)。onChange回调:Tab 切换时同步更新currentTabIndex,从而改变顶部标题与右上角操作。barMode: BarMode.Fixed:固定 Tab 数量(本项目是 3 个),不启用滚动。
6.2 顶部标题栏
@Builder
HeaderBar() {
Row() {
Text(this.headerTitle())
.fontSize(20)
.fontWeight(FontWeight.Bold);
Blank();
if (this.currentTabIndex === 0) {
Text('共 ' + this.reports.length + ' 次').fontSize(13).fontColor('#888888');
} else if (this.currentTabIndex === 1) {
Text(this.reports.length + ' 次记录').fontSize(13).fontColor('#888888');
} else {
Text('⚙️').fontSize(20)
.onClick(() => { router.pushUrl({ url: 'pages/Profile' }); });
}
}
.width('100%').height(56).padding({ left: 16, right: 16 })
.backgroundColor('#FFFFFF');
}
headerTitle() 是一个简单的私有方法,根据当前 Tab 返回不同标题。这种"一处状态,多处响应"是 ArkTS 声明式 UI 的精髓。
Blank() 是 ArkUI 中很实用的组件:它会自动占据父容器中的剩余空间,常用于"左侧 + 右侧对齐"的布局场景。
6.3 Tab 1:报告列表
报告 Tab 的布局从上到下分三段:
- 最新一次报告摘要卡:直接展示最近一次体检的简要结论,方便用户一眼看到自己的健康概况,点击可进入详情。
- "历次体检报告"小标题:配合右侧"按时间倒序"提示。
- List 列表:渲染所有历史报告。
每张报告卡片包含日期、医院、结论(最多 2 行)、关键指标摘要、查看按钮。卡片整体可点击,进入 ReportDetail。
List({ space: 10 }) {
ForEach(this.reports, (report: HealthReport) => {
ListItem() { this.ReportCard(report); }
}, (report: HealthReport) => report.date);
}
.listDirection(Axis.Vertical)
.scrollBar(BarState.Auto)
.edgeEffect(EdgeEffect.Spring)
.padding({ left: 12, right: 12, bottom: 16 })
.layoutWeight(1);
ForEach 的第二个参数是 keyGenerator:传入
(report) => report.date,让 ArkTS 准确识别每一项,避免不必要的重建。如果省略,框架会用数组索引作为 key,但当数组顺序变化时可能导致状态错乱。
edgeEffect(EdgeEffect.Spring) 让用户滑到边界时有弹性回弹效果,提升手感。
指标摘要部分用 Flex({ wrap: FlexWrap.Wrap }) 实现自动换行:
Flex({ wrap: FlexWrap.Wrap }) {
ForEach(report.metrics, (m: MetricItem) => {
Row() {
Text(m.name).fontColor('#888888');
Text(' ' + this.formatValue(m.value) + ' ' + m.unit).fontColor('#222222');
if (m.normalLow > 0 || m.normalHigh > 0) {
Text(' ' + getMetricStatus(m.value, m.normalLow, m.normalHigh))
.fontColor(getMetricStatus(m.value, m.normalLow, m.normalHigh) === '正常' ? '#27AE60' : '#E74C3C');
}
}
.padding({ left: 8, right: 8, top: 4, bottom: 4 })
.backgroundColor('#F5F6FA')
.borderRadius(10)
.margin({ right: 6, bottom: 6 });
}, (m: MetricItem) => m.name);
}
这样无论每条报告有多少指标,都能优雅地排成"标签云"形式。每个标签内:浅灰指标名 + 黑色数值 + 绿色/红色状态文字。
6.4 Tab 2:趋势总览
趋势 Tab 顶部说明"近 N 次体检关键指标",然后用一个 2 列 Grid 展示所有关键指标的小图,每张图都是一个 MetricChart。
Grid() {
ForEach(this.metrics, (item: MetricSnapshot) => {
GridItem() {
MetricChart({ snapshot: item });
}
.padding(6)
.onClick(() => {
router.pushUrl({
url: 'pages/MetricDetail',
params: { name: item.name }
});
});
}, (item: MetricSnapshot) => item.name);
}
.columnsTemplate('1fr 1fr')
.columnsGap(8)
.rowsGap(8)
.padding({ left: 10, right: 10, bottom: 12 });
onClick 钩子挂在 GridItem 上,点击时携带 name 参数跳到 MetricDetail。这是 ArkTS 中最常见的页面跳转写法。
columnsTemplate('1fr 1fr') 表示两列等宽。1fr 是 Grid 特有的单位,表示"按比例分配剩余空间"。如果想 3 列,就写 '1fr 1fr 1fr'。
趋势页底部还放了一个"健康小贴士"卡,包含 3 条小知识:盐摄入、BMI 范围、复查频率。这种"数据 + 知识"的组合,能让应用更有人情味,也体现了"健康 App"应有的价值。
6.5 Tab 3:我的(精简版)
为了避免与 Profile.ets 重复,“我的” Tab 设计成精简版:
- 用户卡(头像 + 姓名 + 基本信息 + "编辑"按钮)
- 数据概览(报告数 / 指标数 / 异常项数)
- 4 宫格快捷入口(我的报告、健康趋势、健康目标、设置)
- 关于(应用版本、帮助、隐私协议)
4 宫格中的"我的报告""健康趋势"可以切换 Tab:
.onClick(() => { this.currentTabIndex = 0; });
而"健康目标""设置"则跳转 Profile 完整页面。这样的设计兼顾了 Tab 切换的便捷性与完整功能的可访问性,是移动端常见的"小入口 + 完整页"模式。
异常项统计是个细节亮点:用 3 个不同颜色(蓝/绿/红)的小数字,让用户在第一眼就能感受到自己的"健康分"。
七、报告详情页:ReportDetail
点击报告卡片后进入 ReportDetail,通过 router.getParams() 拿到 date,再回查报告数据:
aboutToAppear(): void {
const params = router.getParams() as Record<string, Object>;
if (params && params['date']) {
this.date = params['date'] as string;
}
for (let i = 0; i < SAMPLE_REPORTS.length; i++) {
if (SAMPLE_REPORTS[i].date === this.date) {
this.report = SAMPLE_REPORTS[i];
break;
}
}
}
aboutToAppear是 ArkTS 组件的生命周期钩子,组件即将显示前调用。适合做参数解析、状态初始化等操作。
页面分三个卡片:
- 基础信息卡:日期、医院、三个统计小方块(异常项 / 正常项 / 指标总数)。
- 体检结论卡:完整结论文字。
- 详细指标卡:每条指标一行,左侧指标名+参考值,右侧数值+状态。
Column() {
Row() {
Column() {
Text(m.name).fontSize(15).fontWeight(FontWeight.Medium);
Text('参考值 ' + m.normalLow + ' - ' + m.normalHigh + ' ' + m.unit)
.fontSize(11).fontColor('#999999');
}.alignItems(HorizontalAlign.Start);
Blank();
Column() {
Text(this.formatValue(m.value) + ' ' + m.unit)
.fontSize(17).fontWeight(FontWeight.Medium);
Text(getMetricStatus(m.value, m.normalLow, m.normalHigh))
.fontSize(11)
.fontColor(getMetricStatus(m.value, m.normalLow, m.normalHigh) === '正常' ? '#27AE60' : '#E74C3C');
}.alignItems(HorizontalAlign.End);
}
.width('100%').padding({ top: 12, bottom: 12 });
if (m !== this.report.metrics[this.report.metrics.length - 1]) {
Divider().color('#EEEEEE').width('100%').height(1);
}
}
异常项统计是个不错的细节:用三个不同颜色(红/绿/蓝)的小数字,让用户在第一眼就能感受到这次体检的"健康分"。
7.1 路由参数解析的最佳实践
router.getParams() 返回的类型是 Object,必须做类型断言。本项目使用 as Record<string, Object> 把整体断言为字典,再逐字段断言为具体类型。这种"双层断言"是 ArkTS 推荐的写法。
另一种更严谨的方式是定义接口:
interface RouteParams {
date: string;
}
const params = router.getParams() as RouteParams;
但鸿蒙路由的实际返回类型包含系统字段,强制类型转换可能会失败。本项目的写法在工程实践中更稳。
八、指标详情页:MetricDetail
指标详情页是本项目的"高光时刻"——Canvas 大图 + 统计 + 历史列表,三者结合形成一个完整的指标分析视图。
8.1 参数获取
aboutToAppear(): void {
const params = router.getParams() as Record<string, Object>;
if (params && params['name']) {
this.metricName = params['name'] as string;
}
for (let i = 0; i < KEY_METRICS.length; i++) {
if (KEY_METRICS[i].name === this.metricName) {
this.snapshot = KEY_METRICS[i];
break;
}
}
}
8.2 大图绘制
与 MetricChart 不同,大图在原有基础上增加了:
- 4 条横向网格 + 刻度文字(最大、中间、最小)
- X 轴日期标签
- 数据点上方数值标注
- 加粗线条(2.5 px),更醒目
// 折线
ctx.strokeStyle = '#1E90FF';
ctx.lineWidth = 2.5;
ctx.beginPath();
for (let i = 0; i < values.length; i++) {
const x = padL + stepX * i;
const y = padT + chartH - ((values[i] - minV) / (maxV - minV)) * chartH;
if (i === 0) ctx.moveTo(x, y);
else ctx.lineTo(x, y);
}
ctx.stroke();
// 数值标注
for (let i = 0; i < values.length; i++) {
const x = padL + stepX * i;
const y = padT + chartH - ((values[i] - minV) / (maxV - minV)) * chartH;
ctx.fillStyle = '#222222';
ctx.font = '11vp sans-serif';
ctx.textAlign = 'center';
const label = Number.isInteger(values[i]) ? values[i].toString() : values[i].toFixed(1);
ctx.fillText(label, x, y - 8);
}
数值标注的位置 y - 8 表示放在点的正上方 8 像素处。需要根据点的实际半径做微调:4 像素半径 + 4 像素间距 = 8 像素。
8.3 数据摘要
this.StatBlock('最新', this.latestValue(), '#1E90FF');
this.StatBlock('最高', this.maxValue(), '#E74C3C');
this.StatBlock('最低', this.minValue(), '#27AE60');
this.StatBlock('平均', this.avgValue(), '#9B59B6');
四个数字横排,每个用不同颜色(蓝/红/绿/紫)区分,让"最高""最低"在视觉上一眼可辨。
8.4 历次记录
历次记录以列表形式呈现,时间倒序(最新在前),并标注状态(正常/偏高/偏低):
private historyRows(): HistoryRow[] {
if (!this.snapshot) return [];
const rows: HistoryRow[] = [];
for (let i = this.snapshot.values.length - 1; i >= 0; i--) {
const date = i < SAMPLE_REPORTS.length ? SAMPLE_REPORTS[i].date : ('第' + (i + 1) + '次');
const v = this.snapshot.values[i];
const status = this.snapshot.normalLow > 0 || this.snapshot.normalHigh > 0
? getMetricStatus(v, this.snapshot.normalLow, this.snapshot.normalHigh)
: '';
rows.push(new HistoryRow(date, Number.isInteger(v) ? v.toString() : v.toFixed(1), status));
}
return rows;
}
HistoryRow 是一个简单的类,专门用于 ForEach 的 key 生成和列表渲染。
8.5 平均值与极值
虽然"最新/最高/最低"是用户最关心的三个数字,但加上"平均"能让用户对自己长期健康水平有更准确的认识。举个例子:如果一个人血压经常在 130-140 之间波动,那么"最高 140" 看起来很吓人,但"平均 135" 才是更客观的参考。
在本项目里,平均值用 toFixed(1) 保留 1 位小数,比最高最低更精确一些。
九、个人中心:Profile
Profile.ets 是独立页面,与 Tab 3 互补。它提供更完整的功能入口:
- 用户卡(含档案号)
- 健康摘要(报告数 / 追踪指标数 / 异常项数)
- 功能菜单(6 项:我的报告、健康趋势、健康目标、消息提醒、家庭成员、设置)
- 退出登录按钮
private menus: MenuItem[] = [
{ icon: '📋', title: '我的报告', desc: '查看历次体检报告' },
{ icon: '📈', title: '健康趋势', desc: '关键指标趋势分析' },
{ icon: '❤️', title: '健康目标', desc: '设置个人健康目标' },
{ icon: '🔔', title: '消息提醒', desc: '复查与异常提醒' },
{ icon: '🏥', title: '家庭成员', desc: '管理家人健康档案' },
{ icon: '⚙️', title: '设置', desc: '隐私与通用设置' }
];
每项菜单通过 ForEach 渲染,行与行之间用 Divider 分隔,最后一项不显示分割线:
if (index !== undefined && index < this.menus.length - 1) {
Divider().color('#EEEEEE').width('100%').height(1).margin({ left: 36 });
}
index 是 ForEach 提供的第二个参数,可以用来判断是否是最后一项。注意:index 在 ArkTS 中是 number | undefined,所以这里做了空检查。
9.1 菜单项设计
每个菜单项是一个"图标 + 标题 + 描述"的三段式结构:
Row() {
Text(item.icon).fontSize(22).margin({ right: 12 });
Column() {
Text(item.title).fontSize(15).fontColor('#222222');
Text(item.desc).fontSize(11).fontColor('#999999').margin({ top: 2 });
}.alignItems(HorizontalAlign.Start).layoutWeight(1);
Text('›').fontSize(20).fontColor('#CCCCCC');
}
- 图标:用 emoji 表情代替 iconfont 包,省去资源文件;
- 标题:用户最关心的操作名;
- 描述:补充说明,让用户无需点击就能知道这个菜单是干嘛的;
- 右箭头:强烈暗示"可点击进入",符合用户预期。
9.2 用户卡设计
用户卡是个人中心的"门面",放在最顶部:
Row() {
Column() {
Text('张').fontSize(24).fontWeight(FontWeight.Bold).fontColor('#FFFFFF');
}
.width(60).height(60).borderRadius(30).backgroundColor('#1E90FF')
.justifyContent(FlexAlign.Center);
Column() {
Text('张三').fontSize(18).fontWeight(FontWeight.Bold).fontColor('#222222');
Text('男 · 32 岁 · 175cm').fontSize(13).fontColor('#888888').margin({ top: 4 });
Row() {
Text('🆔 ').fontSize(12).fontColor('#888888');
Text('档案号 2024-0088').fontSize(12).fontColor('#888888');
}.margin({ top: 4 });
}.margin({ left: 14 }).alignItems(HorizontalAlign.Start).layoutWeight(1);
}
头像用"姓 + 圆形背景"实现,既能展示身份,又不需要图片资源。档案号暗示"我们有完整的健康档案"。
十、路由与导航
10.1 路由跳转
鸿蒙提供 import router from '@ohos.router' 模块,最常用的两个 API:
// 跳转
router.pushUrl({
url: 'pages/ReportDetail',
params: { date: '2025-03-05' }
});
// 返回
router.back();
pushUrl 的第二个参数 params 可以传递任意可序列化的对象。本项目传字符串(日期、指标名),传对象也是支持的。
10.2 参数接收
在目标页面 aboutToAppear 中读取:
const params = router.getParams() as Record<string, Object>;
if (params && params['date']) {
this.date = params['date'] as string;
}
10.3 main_pages.json
新创建的页面必须在 entry/src/main/resources/base/profile/main_pages.json 中注册,否则无法访问:
{
"src": [
"pages/Index",
"pages/ReportDetail",
"pages/MetricDetail",
"pages/Profile"
]
}
10.4 Tab 状态同步
Tab 内部点击"我的报告"快捷入口时,需要同步 currentTabIndex:
.onClick(() => { this.currentTabIndex = 0; });
@State 的变化会自动触发 UI 重新渲染,Tabs 会平滑地切换到对应 Tab。
10.5 路由方案的扩展
对于更复杂的应用,鸿蒙还提供以下路由能力:
router.replaceUrl:替换当前页面(适合登录后跳转首页);router.clear():清空路由栈;router.getState():获取当前路由信息(用于实现"二次返回退出 App"等逻辑);- Navigation 组件:API 12 引入的声明式路由容器,更易于做转场动画。
本项目用最基础的 pushUrl/back 已经能满足需求,但如果未来要做复杂的转场动画,建议迁移到 Navigation 组件。
十一、UI 设计要点
11.1 颜色系统
整个应用只使用了一组精心挑选的颜色:
- 主色:
#1E90FF(道奇蓝),用于链接、强调、趋势线; - 成功:
#27AE60(绿),用于正常状态; - 警告:
#E74C3C(红),用于异常项; - 中性背景:
#F5F6FA(浅灰),用于页面底色; - 卡片背景:
#FFFFFF(白),用于内容容器; - 文字主色:
#222222;副色:#555555 / #666666 / #888888 / #999999,按重要性递减。
这种克制的色彩系统让界面看起来干净专业,也方便后续做主题切换(深色模式只需要替换这些颜色变量即可)。
11.2 间距与圆角
所有卡片统一使用 10px 圆角,内部 padding 14-16px,卡片之间间距 8-10px。这种"呼吸感"设计让信息密度不会太高,长时间查看也不易疲劳。
Material Design 与 Apple HIG 都强调"留白"的重要性。本项目卡间距选择 10px,是因为移动端屏幕空间宝贵;如果是大屏应用,可以适当放大到 12-16px。
11.3 文字层级
- 大标题:20-22px / Bold;
- 卡片标题:15-17px / Medium;
- 正文:13-14px / Regular;
- 辅助:11-12px / Regular + 灰色。
清晰的层级帮助用户快速识别信息。在 ArkTS 中,字号单位用 vp(virtual pixel)而非 px,可以保证在不同 DPI 屏幕上大小一致。
11.4 状态色与可访问性
红色(#E74C3C)表示异常,绿色(#27AE60)表示正常。但有些色盲用户可能分不清红绿,因此在文案层面也应当明确写出"偏高/偏低/正常",让色觉障碍用户也能使用。这种"颜色 + 文字"双通道传达,是无障碍设计的基本要求。
十二、性能与体验优化
12.1 Canvas 自适应
通过 onAreaChange 监听画布尺寸变化并触发重绘,让趋势图在折叠屏、平板等不同设备上都能正确显示。如果用固定坐标硬编码,在不同分辨率下会出现偏移。
12.2 ForEach keyGenerator
为 ForEach 提供 key 生成函数(如 (item) => item.name),让 ArkTS 准确识别每一项的身份,避免不必要的重建。当数据变化时,框架会复用相同 key 的组件,而不是销毁重建。
12.3 状态外置
页面间共享的数据(如报告列表、关键指标)放在 model 文件中以常量形式导出,避免跨页面状态同步问题。如果用全局状态管理(如 AppStorage),要小心数据更新时机,避免出现"页面 A 已经更新,页面 B 还是旧值"的问题。
12.4 按需渲染
MetricChart 只在 onReady 与 onAreaChange 时绘制,不参与每帧重绘,性能开销极低。这也是 Canvas 相比 SVG 的优势:SVG 重绘需要遍历整个 DOM 树,Canvas 只需要重新执行绘制命令。
12.5 数值格式化
formatValue 统一处理"整数不带小数点、浮点数保留 1 位"的显示逻辑,避免出现 5.0 这种不友好的显示。统一格式化也方便后续做"按用户偏好显示精度"等扩展。
12.6 避免不必要的重绘
ArkTS 的响应式机制会自动追踪 @State 变量的依赖关系。但有时一个 @State 会被多个 UI 引用,修改它会导致大范围重绘。比如 currentTabIndex 既影响顶部标题,又影响 Tab 高亮,又影响 Tab 内部某些条件渲染。修改它是合理的,但如果把整个 Tab 数据都放进 currentTabIndex 就不合理了。
十三、常见问题与解决方案
13.1 @Builder 不支持函数参数
ArkTS 的 @Builder 函数参数有严格限制:不支持函数(Function)类型。本项目曾尝试用 QuickItem(icon, label, callback) 写法,发现编译失败。最终改为内联实现:每个 GridItem 内直接写 Column 和 onClick。
这是个常见的"TypeScript 能写、ArkTS 编译报错"的坑。ArkTS 之所以限制函数类型,主要是为了保证 UI 渲染的可预测性——如果 UI 树里藏着函数闭包,会让框架的优化变得困难。
13.2 Canvas 不显示
最容易踩的坑是 Canvas 区域为 0(width/height 都没设置)。本项目统一使用 width('100%') + height(110),并通过 onAreaChange 拿到真实像素。如果 Canvas 完全不显示,先检查 width/height 是否有值。
13.3 路由参数类型
router.getParams() 返回的是 Object,必须用类型断言转换为具体类型。建议在断言前用 if (params && params['xxx']) 做空检查,避免运行时错误。如果忘了空检查,目标页面在直接进入(无参数)时会因为 undefined.xxx 崩溃。
13.4 页面未注册
新页面在 main_pages.json 中遗漏是新人最常犯的错误。修改后一定要检查该文件。DevEco Studio 在编译时通常会给出警告,但偶尔也会漏报。
13.5 状态文字闪烁
如果某个指标在边界值附近(比如正常上限 120,实际值 119.5、120.5 反复横跳),状态文字会在"正常/偏高"之间闪烁。生产环境里可以对状态判断加一点缓冲(比如 ±0.5 的死区),但本项目为了简化没做。
十四、可扩展方向
完成基础功能后,这个应用还有大量可扩展空间:
-
数据持久化:使用
@ohos.data.relationalStore或@ohos.data.preferences替换示例数据,让用户新增的报告可以本地保存。前者适合结构化数据,后者适合少量配置。 -
真实图表组件:把自绘 Canvas 替换为社区维护的图表库(如
vchart),获得更丰富的图表类型(柱状、饼图、雷达图等)。但要注意图表库通常体积较大,对启动时间有影响。 -
健康提醒:基于异常项触发系统通知,提醒用户复查。这需要用到
Notification能力,并在module.json5中申请相应权限。 -
多用户/家庭账户:扩展为支持多个家庭成员健康档案管理。可以用一个家庭组 ID 关联多个用户。
-
数据导入:支持读取医院出具的 PDF 报告,解析后导入应用。鸿蒙生态里有 PDF 解析库可以使用。
-
云端同步:通过 HarmonyOS 的分布式能力,把数据同步到其他设备。比如在手机上看报告,平板上自动同步。
-
AI 健康建议:接入 LLM,根据用户健康数据给出个性化建议。鸿蒙的 AI 能力 + 端侧大模型可以做到"数据不出端"。
-
可视化大屏:基于鸿蒙的"元服务"卡片能力,把关键指标做成桌面卡片,让用户在不打开 App 的情况下也能看到自己的健康状况。
-
国际化:把所有中文文案抽到
string.json中,并提供英文版本。鸿蒙的 i18n 框架支持多语言切换。 -
健康知识库:把"健康小贴士"扩展为一个完整的知识库,按指标分类,提供饮食、运动、复查等全方位建议。
十五、写在最后
本项目用不到 800 行 ArkTS 代码,实现了"报告列表 + 趋势图 + 报告详情 + 指标详情 + 个人中心"的完整应用。它体现了 ArkTS 开发的核心优势:
- 声明式 UI:用接近自然语言的方式描述界面,代码即文档;
- 强类型系统:编译期捕获错误,运行期更稳定;
- 组件化:自绘图表作为独立组件,可在多处复用;
- 路由系统:页面间跳转简洁,参数传递灵活;
- Canvas 能力:原生支持丰富的图形绘制,无需第三方依赖。
对于想入门鸿蒙应用开发的同学,本项目是一个不错的起点。它不依赖任何第三方组件,覆盖了 ArkTS 的大部分基础语法与 UI 组件,又不失实用性。希望本文能帮助你在鸿蒙开发的道路上迈出坚实的一步。
鸿蒙生态正在快速发展,应用框架、工具链、组件库都在持续完善。站在今天这个时间点,ArkTS 已经成为一种成熟的生产力工具——它既有 TypeScript 的现代化语法,又有鸿蒙的原生能力加持。无论是做企业内部应用、To C 消费品,还是探索元服务、AI Agent,都值得投入时间学习。
源码已开源在
ArkTSReport工程下,欢迎 clone、修改、二次创作。如果觉得本文有帮助,欢迎点赞、收藏、转发,让更多开发者了解鸿蒙生态。
附录:关键文件清单
entry/src/main/ets/model/HealthReport.ets— 数据模型与示例数据entry/src/main/ets/components/MetricChart.ets— 自绘折线图组件entry/src/main/ets/pages/Index.ets— 主页面(Tabs + List + Grid)entry/src/main/ets/pages/ReportDetail.ets— 报告详情entry/src/main/ets/pages/MetricDetail.ets— 指标详情entry/src/main/ets/pages/Profile.ets— 个人中心entry/src/main/resources/base/profile/main_pages.json— 页面路由注册
附录 B:常见 ArkTS API 速查
| 场景 | API | 用途 |
|---|---|---|
| 布局 | Column / Row / Stack |
基础布局容器 |
| 列表 | List / ListItem / ForEach |
长列表渲染 |
| 网格 | Grid / GridItem |
等分网格 |
| 滚动 | Scroll |
任意内容滚动 |
| 状态 | @State / @Prop / @Link / @ObjectLink |
响应式数据 |
| 画布 | Canvas / CanvasRenderingContext2D |
2D 图形绘制 |
| 导航 | router.pushUrl / router.back |
页面跳转 |
| 弹窗 | AlertDialog / ActionSheet / Prompt |
用户交互 |
| 媒体 | Image / Video / Web |
多媒体展示 |
| 数据 | preferences / relationalStore |
本地持久化 |
掌握这张表,80% 的日常开发场景都能覆盖。
附录 C:推荐学习路径
对于 ArkTS 初学者,建议按以下顺序学习:
- 基础语法(1 周):TypeScript + 鸿蒙 API 12 文档中的"ArkTS 语言基础"章节。
- UI 组件(2 周):ArkUI 组件官方文档,从基础组件开始,每个写一个小 demo。
- 状态管理(1 周):理解
@State/@Prop/@Link的差异。 - 路由与导航(3 天):多页面跳转、参数传递、转场动画。
- Canvas / 自定义绘制(1 周):从简单图形开始,逐步复杂化。
- 持久化与网络(1 周):本地数据库、HTTP 请求、WebSocket。
- 分布式能力(1 周):流转、跨设备调用、统一搜索。
- 元服务与卡片(1 周):免安装应用、桌面卡片。
按这个节奏,2-3 个月就能从入门到胜任中等复杂度的鸿蒙应用开发。
十六、测试与调试
任何一个上线的应用都离不开测试。鸿蒙应用支持传统的单元测试、UI 测试与端到端测试,下面简单介绍本项目涉及的测试思路。
16.1 单元测试
ArkTS 支持通过 ohtest 框架编写单元测试,重点测试纯函数、工具类等不依赖 UI 的逻辑。本项目中最值得测试的是 getMetricStatus 函数:
// entry/src/test/LocalUnit.test.ets
import { getMetricStatus } from '../../main/ets/model/HealthReport';
export default function testsuite() {
test('正常范围', () => {
expect(getMetricStatus(120, 90, 120)).assertEqual('正常');
});
test('偏高', () => {
expect(getMetricStatus(125, 90, 120)).assertEqual('偏高');
});
test('偏低', () => {
expect(getMetricStatus(85, 90, 120)).assertEqual('偏低');
});
test('无参考', () => {
expect(getMetricStatus(170, 0, 0)).assertEqual('正常');
});
}
通过单元测试,我们可以放心地修改 getMetricStatus 内部实现,而不必担心破坏现有行为。
16.2 UI 测试
对于组件级别的渲染测试,鸿蒙提供 UITest 框架。它可以模拟用户点击、滑动、输入等操作,并断言 UI 状态变化。本项目暂时没写 UI 测试,因为核心 UI 都是数据驱动的——只要数据正确,UI 必然正确。
16.3 调试技巧
在 DevEco Studio 中调试 ArkTS 应用时,以下技巧能事半功倍:
- 断点调试:在
.ets文件中点击行号设置断点,启动调试模式后会在断点处暂停,可以查看变量值、调用栈。 - HiLog 日志:通过
console.info/console.warn/console.error输出日志,在 DevEco 的 Log 窗口查看。 - Inspector:在真机或模拟器上运行应用时,可以用 Inspector 工具查看 UI 树结构、组件属性、布局信息。
- 性能分析:通过 DevEco Profiler 工具,可以查看 CPU、内存、GPU、网络等性能指标,定位卡顿、内存泄漏等问题。
16.4 常见调试场景
场景一:Canvas 不显示
先用 console.info 输出画布的 canvasWidth 和 canvasHeight,确认是否大于 0。如果一直是 0,说明父容器没有正确传递尺寸,需要检查 Grid 布局配置。
场景二:路由跳转失败
检查 main_pages.json 中是否注册了目标页面;检查 url 字段是否以 pages/ 开头;检查 params 是否可序列化。
场景三:状态不更新
确认变量是否用了 @State / @Prop 装饰器;确认 UI 引用了状态变量(直接引用才会追踪);确认状态变化是在主线程触发的(鸿蒙 UI 更新必须在主线程)。
十七、与其他框架的对比
很多 ArkTS 新人会问:ArkTS 比起 Flutter、React Native、SwiftUI 等其他跨端/原生框架,优势在哪里?下面简单对比一下。
17.1 ArkTS vs Flutter
Flutter 用 Dart 语言,自带渲染引擎(Skia),可以做到"像素级一致"的多端体验。ArkTS 用类 TypeScript 语法,原生调用系统组件。
- 学习成本:ArkTS 入门更简单,前端开发者零成本上手;Dart 对大多数人来说较新。
- 性能:Flutter 在图形密集型场景(复杂动画、自定义绘制)略胜;ArkTS 在系统集成、分布式能力上更强。
- 生态:Flutter 生态成熟、组件丰富;ArkTS 生态正在快速建设中。
- 包体积:Flutter 引入 Skia 后包体积偏大;ArkTS 用系统组件更轻量。
17.2 ArkTS vs React Native
React Native 用 JavaScript + JSX 桥接到原生组件。ArkTS 是更彻底的"原生"。
- 渲染机制:React Native 仍依赖 JS 引擎 + Bridge,性能有损耗;ArkTS 编译为原生代码,无中间层。
- 类型系统:ArkTS 强类型,TypeScript 在 React Native 中仍可能被绕过。
- UI 范式:两者都是声明式 UI,但 ArkTS 的
@State响应式追踪更细粒度。
17.3 ArkTS vs SwiftUI
SwiftUI 是 Apple 的声明式 UI 框架,与 ArkTS 在设计哲学上非常相似(都强调状态驱动 UI)。
- 跨平台:SwiftUI 仅限 Apple 生态;ArkTS 覆盖鸿蒙全家桶。
- 语言:SwiftUI 用 Swift;ArkTS 用 TypeScript-like 语法。前者学习曲线更陡。
- 生态:SwiftUI 生态成熟,组件丰富;ArkTS 在快速追赶中。
17.4 选型建议
- 如果应用只在鸿蒙生态内运行,首选 ArkTS;
- 如果要同时覆盖 iOS/Android,Flutter / React Native 仍然是更成熟的选择;
- 如果是企业内部工具、需要深度集成鸿蒙能力(如分布式、原子化服务),ArkTS 是唯一选项。
十八、部署与发布
开发完成后,下一步就是打包发布。鸿蒙应用的发布流程包括签名、打包、上架三个步骤。
18.1 应用签名
鸿蒙要求所有应用必须经过签名才能安装。开发阶段可以使用 DevEco Studio 提供的自动签名(AGConnect / 调试证书)。发布到应用市场前,需要:
- 在华为开发者联盟申请发布证书;
- 在 DevEco Studio 中配置
Build Profile,选择发布证书; - 构建 HAP(HarmonyOS Ability Package)文件。
18.2 多端发布
鸿蒙应用支持以下发布渠道:
- 华为应用市场(AppGallery):面向消费者的主渠道。
- 企业内部分发:通过 MDM(Mobile Device Management)系统向企业员工推送。
- 快应用 / 元服务:免安装形态,用户无需下载即可使用。
- Web 部署:通过 DevEco Studio 把项目打包为 PWA / H5,在浏览器中运行(部分能力受限)。
本项目是一个普通的鸿蒙 App,可以直接打包成 HAP 上架到应用市场。
18.3 版本管理
在 AppScope/app.json5 中维护版本信息:
{
"app": {
"versionName": "1.0.0",
"versionCode": 1
}
}
每次发版前需要递增 versionCode(整数)与 versionName(用户可见的版本号)。
18.4 灰度发布
华为应用市场支持按比例、按地域、按机型灰度发布新版本。建议新功能先以 5% 灰度上线,观察无问题后再逐步放大。
十九、项目源码精读
在结束本文之前,我们快速过一遍项目源码的关键片段,帮助读者在本地工程中快速定位。
19.1 数据流
[model/HealthReport.ets] -- 导出 SAMPLE_REPORTS / KEY_METRICS
↓
[pages/Index.ets] -- @State reports / metrics 持有
↓
┌────┴────┐
[List/Grid] [components/MetricChart] -- 子组件接收
↓
[user click] -- router.pushUrl 跳转
↓
[pages/ReportDetail / MetricDetail / Profile] -- 独立页面
整个数据流是单向的:数据从模型流入页面,用户的交互产生新事件,事件触发跳转或状态更新。这种单向数据流让应用行为可预测、易调试。
19.2 关键文件行数
为了让你对项目体量有直观感受,列出主要文件的大致行数:
HealthReport.ets:约 100 行MetricChart.ets:约 180 行Index.ets:约 350 行ReportDetail.ets:约 200 行MetricDetail.ets:约 280 行Profile.ets:约 150 行main_pages.json/string.json/module.json5:约 80 行
总计不到 1400 行的代码(含注释),实现了一个完整的多页面应用。这正是 ArkTS 声明式 UI 的魅力:少即是多。
19.3 代码风格
本项目遵循以下代码风格:
- 缩进:4 空格
- 引号:单引号优先
- 命名:组件名 PascalCase,变量/函数 camelCase,常量 UPPER_SNAKE_CASE
- 注释:行注释用
//,块注释用/* */,组件顶部用// xxx:xxx简述用途 - 空行:函数/Builder 之间空一行
- 类型:所有变量必须有明确类型,不使用
any
这些风格与鸿蒙官方示例保持一致,方便与社区代码交流。
二十、致谢与参考
本文写作过程中参考了以下资源:
- 鸿蒙官方文档:developer.huawei.com/consumer/cn/hmos
- ArkTS 语言规范:gitee.com/openharmony/docs
- ArkUI 组件参考:developer.huawei.com/consumer/cn/doc/harmonyos-guides
- DevEco Studio 用户指南
感谢鸿蒙生态的开发者们,是你们的代码与分享让这个生态越来越丰富。
写在最后:健康是人生最宝贵的财富。希望这款小小的 App,能帮你更好地管理自己的健康数据,也希望你通过本文的旅程,对鸿蒙开发充满信心。未来的你,会感谢现在努力学习的自己。加油!
更多推荐

所有评论(0)