
养猫的朋友都知道,疫苗接种是猫咪健康管理中非常重要的一环。今天我们来实现一个疫苗记录功能,帮助铲屎官们追踪猫咪的疫苗接种情况。
功能规划
在开始写代码之前,先想清楚这个页面需要哪些功能:
- 展示疫苗相关的科普知识
- 显示已有的疫苗接种记录列表
- 支持添加新的疫苗记录
- 空状态时给出友好提示
这样规划下来,整个页面的结构就比较清晰了。
页面基础结构
首先搭建页面的基本框架:
dart
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:intl/intl.dart';
import '../../providers/health_provider.dart';
import '../../models/health_record.dart';
import 'add_health_record_screen.dart';
这里引入了几个关键依赖,provider用于状态管理,screenutil处理屏幕适配,intl负责日期格式化。
把这些工具组合起来,能让开发效率提升不少。
接下来定义疫苗记录页面的主体类:
dart
class VaccinationScreen extends StatelessWidget {
final String catId;
const VaccinationScreen({super.key, required this.catId});
这是一个无状态组件,通过catId来标识当前是哪只猫咪的疫苗记录。
使用StatelessWidget是因为数据变化由Provider来管理,页面本身不需要维护状态。
构建主界面
页面的build方法是整个UI的入口:
dart
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('疫苗记录')),
body: Consumer<HealthProvider>(
builder: (context, provider, child) {
final records = provider.getRecordsByType(catId, HealthRecordType.vaccination);
Scaffold提供了Material Design的基础页面结构,包含AppBar和body区域。
Consumer组件监听HealthProvider的变化,当数据更新时自动重建UI。
获取到记录后,需要判断是否为空:
dart
if (records.isEmpty) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.vaccines, size: 80.sp, color: Colors.grey[300]),
SizedBox(height: 16.h),
Text('暂无疫苗记录', style: TextStyle(color: Colors.grey[600])),
空状态的处理很重要,用一个大图标配合文字提示,让用户知道当前没有数据。
使用.sp和.h单位是screenutil的适配写法,能在不同屏幕上保持一致的视觉效果。
空状态下还要提供添加按钮:
dart
SizedBox(height: 16.h),
ElevatedButton(
onPressed: () => _addRecord(context),
child: const Text('添加疫苗记录'),
),
],
),
);
}
在空状态页面放置添加按钮,引导用户进行第一次记录。
这种设计比单纯显示"暂无数据"要友好得多。
有数据时的列表展示
当有疫苗记录时,展示列表内容:
dart
return ListView(
padding: EdgeInsets.all(16.w),
children: [
_buildVaccineInfo(),
SizedBox(height: 16.h),
Text('疫苗记录', style: TextStyle(fontSize: 16.sp, fontWeight: FontWeight.bold)),
SizedBox(height: 8.h),
...records.map((r) => _buildRecordCard(r)),
],
);
},
),
ListView作为滚动容器,内部先放疫苗知识卡片,再放记录列表。
使用展开运算符...将记录列表映射为卡片组件。
页面底部添加悬浮按钮:
dart
floatingActionButton: FloatingActionButton(
onPressed: () => _addRecord(context),
backgroundColor: Colors.orange,
child: const Icon(Icons.add),
),
);
}
FloatingActionButton是Material Design的标准添加按钮样式。
橙色主题与整个App的风格保持一致。
疫苗知识卡片
这个卡片展示一些基础的疫苗科普:
dart
Widget _buildVaccineInfo() {
return Card(
color: Colors.blue[50],
child: Padding(
padding: EdgeInsets.all(16.w),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(Icons.info, color: Colors.blue, size: 20.sp),
SizedBox(width: 8.w),
Text('疫苗知识', style: TextStyle(fontWeight: FontWeight.bold, color: Colors.blue[800])),
],
),
卡片使用浅蓝色背景,与疫苗主题相呼应。
标题行用Row布局,图标和文字水平排列。
知识内容部分:
dart
SizedBox(height: 8.h),
Text(
'• 猫三联:预防猫瘟、猫鼻支、猫杯状病毒\n'
'• 狂犬疫苗:预防狂犬病\n'
'• 首次免疫:8周龄开始,间隔3-4周接种\n'
'• 加强免疫:每年一次',
style: TextStyle(fontSize: 13.sp, color: Colors.blue[700]),
),
],
),
),
);
}
用项目符号列出关键信息,简洁明了。
这些都是养猫必备的基础知识,放在这里方便用户随时查看。
记录卡片组件
每条疫苗记录用卡片形式展示:
dart
Widget _buildRecordCard(HealthRecord record) {
return Card(
margin: EdgeInsets.only(bottom: 8.h),
child: ListTile(
leading: CircleAvatar(
backgroundColor: Colors.blue[100],
child: Icon(Icons.vaccines, color: Colors.blue, size: 20.sp),
),
title: Text(record.title),
Card配合ListTile是展示列表项的经典组合。
leading位置放圆形头像,用疫苗图标作为视觉标识。
副标题显示日期和医院信息:
dart
subtitle: Text(
'${DateFormat('yyyy-MM-dd').format(record.date)}${record.hospital != null ? ' · ${record.hospital}' : ''}',
),
日期格式化为年月日形式,如果有医院信息就一并显示。
用三元表达式处理可选字段,代码更简洁。
右侧显示下次接种时间:
dart
trailing: record.nextDate != null
? Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Text('下次', style: TextStyle(fontSize: 10.sp, color: Colors.grey)),
Text(DateFormat('MM-dd').format(record.nextDate!), style: TextStyle(color: Colors.orange)),
],
)
: null,
),
);
}
下次接种日期用橙色高亮显示,提醒用户注意。
如果没有设置下次日期,trailing就为null,不显示任何内容。
添加记录的导航
点击添加按钮时跳转到添加页面:
dart
void _addRecord(BuildContext context) {
Navigator.push(context, MaterialPageRoute(
builder: (_) => AddHealthRecordScreen(catId: catId, initialType: HealthRecordType.vaccination),
));
}
}
使用Navigator.push进行页面跳转,传入catId和初始类型。
initialType设为vaccination,添加页面会默认选中疫苗类型。
数据模型设计
疫苗记录复用了健康记录的数据模型:
dart
enum HealthRecordType {
vaccination,
deworming,
checkup,
treatment,
other,
}
枚举类型定义了几种健康记录类型,vaccination就是疫苗。
这样设计可以让不同类型的健康记录共用一套逻辑。
记录模型的核心字段:
dart
class HealthRecord {
final String id;
final String catId;
final HealthRecordType type;
final String title;
final DateTime date;
final DateTime? nextDate;
final String? hospital;
final String? notes;
包含了记录的基本信息,nextDate和hospital是可选字段。
使用final修饰保证数据不可变,符合Flutter的最佳实践。
Provider状态管理
HealthProvider负责管理所有健康记录:
dart
class HealthProvider extends ChangeNotifier {
final List<HealthRecord> _records = [];
List<HealthRecord> getRecordsByType(String catId, HealthRecordType type) {
return _records
.where((r) => r.catId == catId && r.type == type)
.toList()
..sort((a, b) => b.date.compareTo(a.date));
}
getRecordsByType方法按猫咪ID和类型筛选记录。
返回前按日期降序排列,最新的记录显示在最前面。
添加记录的方法:
dart
void addRecord(HealthRecord record) {
_records.add(record);
notifyListeners();
}
添加记录后调用notifyListeners通知所有监听者。
Consumer组件会自动重建UI,展示新添加的记录。
日期处理技巧
日期格式化是这个页面的常见操作:
dart
DateFormat('yyyy-MM-dd').format(record.date)
intl包提供的DateFormat类支持各种日期格式。
yyyy-MM-dd是最常用的年月日格式。
下次接种日期用简短格式:
dart
DateFormat('MM-dd').format(record.nextDate!)
只显示月日,因为年份通常是当年或明年,不需要特别标注。
感叹号表示这里已经确认nextDate不为null。
屏幕适配方案
screenutil的使用贯穿整个页面:
dart
Icon(Icons.vaccines, size: 80.sp, color: Colors.grey[300])
SizedBox(height: 16.h)
EdgeInsets.all(16.w)
.sp用于字体和图标大小,会根据屏幕宽度等比缩放。
.h和.w分别用于高度和宽度,确保在不同设备上布局一致。
颜色主题设计
页面使用了蓝色系作为疫苗相关的主题色:
dart
Colors.blue[50] // 卡片背景
Colors.blue[100] // 头像背景
Colors.blue[700] // 文字颜色
Colors.blue[800] // 标题颜色
不同深浅的蓝色形成层次感。
与橙色的强调色形成对比,突出重要信息。
空状态设计思路
空状态不只是显示"暂无数据":
dart
Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.vaccines, size: 80.sp, color: Colors.grey[300]),
SizedBox(height: 16.h),
Text('暂无疫苗记录', style: TextStyle(color: Colors.grey[600])),
SizedBox(height: 16.h),
ElevatedButton(
onPressed: () => _addRecord(context),
child: const Text('添加疫苗记录'),
),
],
),
)
大图标让页面不会显得太空。
添加按钮引导用户进行下一步操作。
ListTile的灵活运用
ListTile是Flutter中非常实用的组件:
dart
ListTile(
leading: CircleAvatar(...),
title: Text(record.title),
subtitle: Text(...),
trailing: Column(...),
)
leading、title、subtitle、trailing四个位置可以放置不同内容。
自动处理间距和对齐,省去很多布局代码。
条件渲染技巧
根据数据是否存在决定显示内容:
dart
record.hospital != null ? ' · ${record.hospital}' : ''
三元表达式处理可选字段的显示。
如果hospital为null,就显示空字符串。
trailing的条件渲染:
dart
trailing: record.nextDate != null
? Column(...)
: null,
当nextDate为null时,trailing也为null,不占用空间。
这种写法比用Visibility组件更简洁。
小结
疫苗记录页面虽然功能不复杂,但涉及到的知识点不少:
- Consumer监听Provider数据变化
- 空状态和有数据状态的切换
- Card和ListTile的组合使用
- 日期格式化和条件渲染
把这些基础打牢,后面做更复杂的功能就会轻松很多。
欢迎加入OpenHarmony跨平台开发社区,一起交流Flutter开发经验: