【Flutter for OpenHarmony 跨平台征文】第三方库 Flutter 血压历史记录列表实战:List组件与空状态设计的鸿蒙开发指南
本文是一篇关于使用Flutter开发OpenHarmony血压历史记录列表的实战指南。文章从需求分析入手,详细介绍了列表功能的实现过程,包括: 核心功能需求:展示血压记录数据、支持列表渲染、空状态设计和操作入口 UI设计:提供了清晰的设计稿和数据接口定义 代码实现:重点讲解了历史记录页面结构、空状态组件和历史记录项组件的开发 关键技术点:列表分隔线对齐、数据更新机制和性能优化建议 文章特别强调了空
【Flutter for OpenHarmony 跨平台征文】Flutter 血压历史记录列表实战:List组件与空状态设计的鸿蒙开发指南
🎯 写在前面
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
👋 自我介绍
嗨,大家好!,上海某高校大一计算机专业的学生 🚀。前面两篇文章我们讲了:
- 《血压数据模型 + WHO分类算法》
- 《血压录入表单 + 实时预览》
这次来讲讲 历史记录列表 的实现 📋。
说实话,List 组件看起来简单,但里面的坑一点都不少:
- 空状态怎么设计
- 分隔线怎么对齐
- 数据怎么更新
- 性能怎么优化
作为一个刚入门的新手,我在这个功能上摔了好几次 😅,今天把踩坑经历分享出来!
一、历史记录列表需求分析
1.1 功能需求
历史记录列表需要展示用户所有的血压测量记录:
| 需求 | 说明 |
|---|---|
| 数据展示 | 显示日期、时间、血压值、脉搏、分类标签 |
| 列表渲染 | 支持多条记录的展示 |
| 空状态 | 无记录时的友好提示 |
| 操作入口 | 点击查看详情(后续扩展) |
1.2 UI 设计稿
┌─────────────────────────────────┐
│ 🩸 血压记录 │
├─────────────────────────────────┤
│ [记录] [趋势] [历史] │
├─────────────────────────────────┤
│ ┌─────────────────────────────┐ │
│ │ 🩸 04-30 08:30 │ │
│ │ 脉搏: 72 bpm │ │
│ │ 120/80 mmHg │ │
│ └─────────────────────────────┘ │
│ ─────────────────────────────── │
│ ┌─────────────────────────────┐ │
│ │ 🩸 04-29 20:15 │ │
│ │ 脉搏: 68 bpm │ │
│ │ 135/88 mmHg │ │
│ └─────────────────────────────┘ │
│ ─────────────────────────────── │
│ ...更多记录... │
└─────────────────────────────────┘
1.3 数据结构
// 单条历史记录
interface HistoryItem {
id: string
date: Date // 日期时间
systolic: number // 收缩压
diastolic: number // 舒张压
pulse: number // 脉搏
status: BPStatus // 分类状态
}
二、完整代码实现
2.1 历史记录页面结构
// ============================================
// 历史记录Tab
// ============================================
@Builder
HistoryTab() {
Column() {
// ============================================
// 空状态:没有记录时显示
// ============================================
if (this.records.length === 0) {
this.EmptyState()
// ============================================
// 记录列表:有记录时显示
// ============================================
} else {
List() {
ForEach(this.records, (record: BloodPressureRecord, index: number) => {
ListItem() {
this.HistoryItem(record)
}
})
}
.width('100%')
.layoutWeight(1) // 占满剩余空间
.divider({
strokeWidth: 0.5,
color: '#F0F0F0',
startMargin: 70, // 和列表项内容对齐
endMargin: 16 // 和列表项右边距对齐
})
}
}
.width('100%')
.height('100%')
.padding({ top: 15 })
.backgroundColor('#F5F7FA')
}
2.2 空状态组件
空状态是 UI 设计中很重要但经常被忽略的部分。当用户第一次使用 App 时,历史列表是空的,这时候要给用户一个友好的引导 😃。
// ============================================
// 空状态占位组件
// 当没有记录时显示,引导用户添加第一条记录
// ============================================
@Builder
EmptyState() {
Column() {
// 血压图标
Text('🩸')
.fontSize(64)
.margin({ bottom: 16 })
// 主提示文字
Text('暂无血压记录')
.fontSize(16)
.fontColor('#999999')
// 副提示文字
Text('开始记录您的血压数据')
.fontSize(14)
.fontColor('#CCCCCC')
.margin({ top: 8 })
}
.width('100%')
.layoutWeight(1) // 占满剩余空间
.justifyContent(FlexAlign.Center) // 垂直居中
}
2.3 历史记录项组件
这是列表中每一行的展示组件:
// ============================================
// 历史记录项组件
// 展示单条血压记录的所有信息
// ============================================
@Builder
HistoryItem(record: BloodPressureRecord) {
Row() {
// ============================================
// 左侧:图标容器
// ============================================
// 背景圆形
Circle()
.width(48)
.height(48)
.fill('#FFEBEE')
// 图标(定位在圆形中间)
Text('🩸')
.fontSize(24)
.position({ x: 12, y: 12 })
// ============================================
// 中间:详细信息
// ============================================
Column() {
// 日期时间
Text(
this.healthService.formatDate(record.date) + ' ' + record.formattedTime
)
.fontSize(15)
.fontWeight(FontWeight.Medium)
.fontColor('#333333')
// 脉搏(如果有的话)
if (record.pulse) {
Text(`脉搏: ${record.pulse} bpm`)
.fontSize(12)
.fontColor('#999999')
.margin({ top: 4 })
}
}
.layoutWeight(1) // 占满中间空间
.alignItems(HorizontalAlign.Start)
.margin({ left: 12 })
// ============================================
// 右侧:血压值
// ============================================
Column() {
// 血压数值(带颜色)
Text(`${record.systolic}/${record.diastolic}`)
.fontSize(20)
.fontWeight(FontWeight.Bold)
.fontColor(this.getCategoryColor(record))
// 单位
Text('mmHg')
.fontSize(11)
.fontColor('#999999')
}
}
.width('94%')
.padding({ top: 12, bottom: 12 })
.backgroundColor('#FFFFFF')
.borderRadius(12)
.margin({ left: '3%', right: '3%', top: 8 })
}
2.4 辅助方法
// ============================================
// 辅助方法
// ============================================
// 获取血压分类颜色
getCategoryColor(record: BloodPressureRecord): string {
return BloodPressureCategoryColor[record.status]
}
// 获取血压分类文字
getCategoryText(record: BloodPressureRecord): string {
return BPStatusDisplay[record.status].text
}
三、List 组件的高级用法
3.1 List 组件 vs ListContainer
在 Flutter for OpenHarmony 中,列表组件有两个选择:
| 组件 | 适用场景 | 特点 |
|---|---|---|
| List | 简单列表 | 轻量,API 简洁 |
| ListContainer | 复杂列表 | 支持自定义布局 |
| Grid | 网格列表 | 需要网格布局时 |
我们这里选择 List 组件,因为它更轻量且足够满足需求。
3.2 分隔线配置
分隔线是列表中很重要的视觉元素,可以让列表项之间有清晰的界限:
List() {
ForEach(this.records, (record: BloodPressureRecord, index: number) => {
ListItem() {
this.HistoryItem(record)
}
})
}
.divider({
strokeWidth: 0.5, // 分隔线粗细
color: '#F0F0F0', // 分隔线颜色(浅灰色)
startMargin: 70, // 开始位置(和内容对齐)
endMargin: 16 // 结束位置(和右边距对齐)
})
3.3 分隔线对齐问题
重点来了! 分隔线对齐是我踩的第一个大坑 😱。
问题描述:
分隔线从屏幕最左边开始,但列表项内容有缩进,导致分隔线和内容对不上:
❌ 错误效果:
|---------分隔线(从左边开始)-----|
| 🩸 04-30 08:30 120/80 |
|---------分隔线(从左边开始)-----|
| 🩸 04-29 20:15 135/88 | ← 这里缩进了,分隔线却没缩进
解决方案:
使用 startMargin 让分隔线和内容对齐:
.divider({
strokeWidth: 0.5,
color: '#F0F0F0',
startMargin: 70, // = 3%(左边距)+ 48(图标宽度)+ 12(图标右margin)
endMargin: 16 // 和列表项右边的 padding 对齐
})
3.4 滚动控制
List() {
ForEach(this.records, (record: BloodPressureRecord, index: number) => {
ListItem() {
this.HistoryItem(record)
}
})
}
.width('100%')
.layoutWeight(1)
.scrollBar(BarState.Auto) // 自动显示滚动条
.edgeEffect(EdgeEffect.Spring) // 弹性效果
四、空状态设计指南
4.1 为什么要设计空状态?
空状态是 UI 设计中非常重要的部分,但经常被忽略:
| 场景 | 没有空状态 | 有空状态 |
|---|---|---|
| 首次使用 | 白屏,用户懵了 | 引导用户添加记录 |
| 删除全部 | 白屏,尴尬 | 提示可以重新开始 |
| 筛选无结果 | 空白,不确定是否出错 | 提示筛选条件 |
4.2 空状态设计原则
| 原则 | 说明 | 示例 |
|---|---|---|
| 图标要醒目 | 用大图标传达情感 | 🩸 64px |
| 文字要引导 | 告诉用户该做什么 | “开始记录您的血压” |
| 按钮可选 | 提供快捷操作入口 | “添加第一条记录” |
| 配色要柔和 | 不要用警示色 | 灰色系 |
4.3 空状态组件的居中技巧
@Builder
EmptyState() {
Column() {
// ... 内容 ...
}
.width('100%')
.layoutWeight(1) // ① 占满剩余空间
.justifyContent(FlexAlign.Center) // ② 垂直居中
}
关键是 .layoutWeight(1) + .justifyContent(FlexAlign.Center) 的组合!
五、数据更新机制
5.1 新增记录后更新列表
这是另一个大坑!添加记录后,列表没有自动更新 😱。
问题描述:
// 保存记录
saveRecord(): void {
// ... 验证逻辑 ...
// 添加到服务
this.healthService.addBloodPressureRecord(...)
// ❌ 列表没有更新!
}
解决方案:
必须同时更新 @State 变量和健康服务的数据:
saveRecord(): void {
// ... 验证逻辑 ...
// 添加到健康服务
const record = this.healthService.addBloodPressureRecord(...)
// ✅ 关键:同时更新 @State 变量
this.records.unshift(record)
// 清空表单
this.clearInputs()
}
5.2 数据流图
用户点击保存
↓
保存到健康服务(持久化)
↓
更新 @State records(触发UI刷新)
↓
List 组件重新渲染
5.3 删除记录的实现
如果需要支持删除功能:
// 删除记录
deleteRecord(id: string): void {
// 从健康服务删除
this.healthService.deleteBloodPressureRecord(id)
// 从 @State 变量删除
const index = this.records.findIndex(r => r.id === id)
if (index !== -1) {
this.records.splice(index, 1)
}
}
六、性能优化
6.1 ForEach 的正确用法
// ✅ 正确的 ForEach 用法
ForEach(this.records, (record: BloodPressureRecord, index: number) => {
ListItem() {
this.HistoryItem(record)
}
}, (record: BloodPressureRecord) => record.id) // 指定唯一的 key
6.2 避免重复渲染
// ❌ 每次渲染都创建新对象
ListItem() {
Text(record.display) // record.display 每次都计算
}
// ✅ 预先计算
ListItem() {
Text(record.display)
}
6.3 懒加载优化
如果记录很多,可以考虑懒加载:
List() {
// 只渲染可见区域的内容
LazyForEach(this.records, (record: BloodPressureRecord) => {
ListItem() {
this.HistoryItem(record)
}
}, (record: BloodPressureRecord) => record.id)
}
七、开发踩坑与解决方案
7.1 踩坑一:分隔线不对齐 😱
问题描述:
分隔线从屏幕最左边开始,但列表项内容有缩进,看起来歪歪扭扭的。
崩溃现场:
|---------分隔线(偏左)--------|
| 🩸 04-30 120/80 | ← 内容偏右
|---------分隔线(偏左)--------|
| 🩸 04-29 135/88 |
解决方案:
.divider({
strokeWidth: 0.5,
color: '#F0F0F0',
startMargin: 70, // 和图标 + 左边距对齐
endMargin: 16 // 和右边距对齐
})
7.2 踩坑二:空状态不居中 😅
问题描述:
空状态组件没有垂直居中,出现在页面顶部。
排查过程:
// ❌ 缺少关键样式
@Builder
EmptyState() {
Column() {
// ...
}
// 缺少 .layoutWeight(1) 和 .justifyContent(FlexAlign.Center)
}
// ✅ 正确写法
@Builder
EmptyState() {
Column() {
// ...
}
.width('100%')
.layoutWeight(1) // 占满剩余空间
.justifyContent(FlexAlign.Center) // 垂直居中
}
7.3 踩坑三:列表项背景色不生效 🎨
问题描述:
给列表项设置了背景色,但不生效。
排查过程:
在 Flutter for OpenHarmony 中,ListItem 本身没有背景色属性,需要在外层容器设置:
// ❌ 错误的写法
ListItem() {
Row()
.backgroundColor('#FFFFFF') // 这个不会生效
}
// ✅ 正确的写法
ListItem() {
Row() {
// ... 内容 ...
}
.width('94%')
.backgroundColor('#FFFFFF') // 设置在 Row 上
.borderRadius(12)
}
7.4 踩坑四:数据更新后列表不刷新 🔄
问题描述:
添加记录后,历史列表没有更新。
解决方案:
// ✅ 确保同时更新服务和 UI 状态
saveRecord(): void {
const record = this.healthService.addBloodPressureRecord(...)
// 关键:同时更新 @State 变量
this.records.unshift(record)
}
7.5 踩坑五:ForEach 缺少 key 🤔
问题描述:
列表渲染时出现奇怪的 bug,有时候会闪烁。
解决方案:
// ✅ 始终提供唯一的 key 函数
ForEach(
this.records,
(record: BloodPressureRecord, index: number) => {
ListItem() {
this.HistoryItem(record)
}
},
(record: BloodPressureRecord) => record.id // 唯一 key
)
八、鸿蒙专属适配
8.1 长按菜单
在鸿蒙设备上,可以实现长按弹出菜单:
ListItem() {
// ...
}
.onLongPress(() => {
// 显示操作菜单
promptAction.showToast({ message: '长按了记录项' })
})
8.2 滑动操作
如果需要支持滑动删除,可以用 GestureDetector:
ListItem() {
GestureDetector() {
// ... 内容 ...
}
.gesture(PanGestureRecognizer())
.onAction((event) => {
// 处理滑动
})
}
8.3 分布式数据展示
鸿蒙的分布式能力可以让数据在多个设备间同步:
// 伪代码:分布式数据展示
import distributedData from '@ohos.data.distributedData'
// 从其他设备获取血压数据
async function fetchDistributedData() {
const kvStore = await distributedData.createKVStore('blood_pressure_db')
const data = await kvStore.get('records')
this.records = JSON.parse(data)
}
九、最终实现效果


9.1 功能验证清单
| 验证项 | 期望效果 | 验证结果 |
|---|---|---|
| 空状态显示 | 无记录时显示引导页 | ✅ |
| 空状态居中 | 引导内容垂直居中 | ✅ |
| 列表正常渲染 | 有记录时正常显示列表 | ✅ |
| 分隔线对齐 | 分隔线和内容对齐 | ✅ |
| 记录项布局 | 日期、脉搏、血压值正确显示 | ✅ |
| 状态颜色 | 不同分类显示不同颜色 | ✅ |
| 新增更新 | 添加记录后列表立即更新 | ✅ |
9.2 视觉效果
| 元素 | 样式 |
|---|---|
| 列表背景 | #F5F7FA(浅灰) |
| 列表项背景 | #FFFFFF(白色) |
| 列表项圆角 | 12px |
| 列表项间距 | 8px |
| 图标容器 | 48px 圆形,#FFEBEE 背景 |
| 血压值颜色 | 根据分类动态变色 |
十、个人总结
10.1 学习心得
历史记录列表的实现比我想的复杂多了 😅。
最大的收获是对 List 组件 有了更深的理解:
- 知道了
.divider()的startMargin和endMargin怎么用 - 学会了空状态组件的设计方法
- 理解了
@State变量和 UI 更新的关系
10.2 核心要点回顾
- 分隔线用 startMargin 对齐:分隔线默认通栏,需要用 startMargin 缩进
- 空状态用 layoutWeight + justifyContent 居中:组合使用才能垂直居中
- 数据更新要同步:更新服务数据的同时必须更新 @State 变量
- ForEach 要提供 key:避免渲染问题
10.3 后续计划
历史列表搞定了,接下来是最后一个功能:
- 📊 趋势统计图表(可视化才是真的难!)
敬请期待!
创作日期:2026 年 4 月
版权所有,转载须注明出处
更多推荐




所有评论(0)