【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()startMarginendMargin 怎么用
  • 学会了空状态组件的设计方法
  • 理解了 @State 变量和 UI 更新的关系

10.2 核心要点回顾

  1. 分隔线用 startMargin 对齐:分隔线默认通栏,需要用 startMargin 缩进
  2. 空状态用 layoutWeight + justifyContent 居中:组合使用才能垂直居中
  3. 数据更新要同步:更新服务数据的同时必须更新 @State 变量
  4. ForEach 要提供 key:避免渲染问题

10.3 后续计划

历史列表搞定了,接下来是最后一个功能:

  • 📊 趋势统计图表(可视化才是真的难!)

敬请期待!


创作日期:2026 年 4 月
版权所有,转载须注明出处

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐