Flutter for OpenHarmony 猫咪管家App实战 - 添加健康记录实现
本文介绍了一个猫咪健康记录管理系统的添加记录功能实现。通过Flutter框架开发,包含表单验证、日期选择、下拉菜单等交互组件。关键功能点包括: 支持疫苗、驱虫、体检等多种记录类型选择 包含标题、日期等必填字段和医院、医生等可选字段 实现表单验证和日期选择器 采用Provider进行状态管理 使用ScreenUtil适配不同屏幕尺寸 代码结构清晰,包含初始化、资源释放等标准实践,为用户提供完整的健康

猫咪的健康是铲屎官最关心的事情。疫苗、驱虫、体检、用药,每一项都需要详细记录。今天我们来实现添加健康记录的功能,这是一个比较复杂的表单页面,涉及多种输入类型。
功能需求分析
添加健康记录页面需要支持以下功能:
- 选择记录类型(疫苗、驱虫、体检等)
- 输入标题和详细描述
- 选择记录日期和下次提醒日期
- 填写医院、医生、费用等可选信息
把这些需求理清楚,实现起来就有方向了。
依赖引入
首先导入需要的包:
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';
Material是Flutter的UI基础库,Provider负责状态管理。
intl包用于日期格式化,screenutil处理屏幕适配。
有状态组件定义
添加页面需要管理表单状态:
class AddHealthRecordScreen extends StatefulWidget {
final String catId;
final HealthRecordType? initialType;
const AddHealthRecordScreen({super.key, required this.catId, this.initialType});
State<AddHealthRecordScreen> createState() => _AddHealthRecordScreenState();
}
catId标识是给哪只猫咪添加记录。
initialType是可选参数,从疫苗或驱虫页面跳转时会传入默认类型。
状态变量声明
State类中定义各种控制器和状态:
class _AddHealthRecordScreenState extends State<AddHealthRecordScreen> {
final _formKey = GlobalKey<FormState>();
final _titleController = TextEditingController();
final _descriptionController = TextEditingController();
final _hospitalController = TextEditingController();
final _doctorController = TextEditingController();
final _costController = TextEditingController();
late HealthRecordType _type;
DateTime _date = DateTime.now();
DateTime? _nextDate;
GlobalKey用于表单验证,五个TextEditingController管理不同的输入框。
_nextDate是可选的,用于设置下次提醒日期。
初始化方法
initState中设置默认类型:
void initState() {
super.initState();
_type = widget.initialType ?? HealthRecordType.checkup;
}
如果传入了initialType就使用它,否则默认是体检类型。
late关键字让_type可以在initState中初始化。
资源释放
dispose中释放所有控制器:
void dispose() {
_titleController.dispose();
_descriptionController.dispose();
_hospitalController.dispose();
_doctorController.dispose();
_costController.dispose();
super.dispose();
}
每个TextEditingController都需要手动释放。
这是避免内存泄漏的标准做法。
页面主体结构
build方法构建整体布局:
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('添加健康记录')),
body: Form(
key: _formKey,
child: SingleChildScrollView(
padding: EdgeInsets.all(16.w),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Form包裹整个表单,用于统一验证。
SingleChildScrollView让内容可以滚动,防止键盘弹出时遮挡。
记录类型选择
下拉框选择健康记录类型:
DropdownButtonFormField<HealthRecordType>(
value: _type,
decoration: const InputDecoration(
labelText: '记录类型 *',
border: OutlineInputBorder(),
prefixIcon: Icon(Icons.category),
),
items: HealthRecordType.values.map((type) {
return DropdownMenuItem(value: type, child: Text(_getTypeString(type)));
}).toList(),
onChanged: (value) => setState(() => _type = value!),
),
SizedBox(height: 16.h),
DropdownButtonFormField是带表单验证的下拉框。
遍历枚举值生成所有选项。
标题输入框
必填的标题字段:
TextFormField(
controller: _titleController,
decoration: const InputDecoration(
labelText: '标题 *',
hintText: '如:猫三联疫苗',
border: OutlineInputBorder(),
prefixIcon: Icon(Icons.title),
),
validator: (value) => value?.isEmpty ?? true ? '请输入标题' : null,
),
SizedBox(height: 16.h),
hintText给出输入示例,帮助用户理解。
validator进行非空验证,星号表示必填。
日期选择器
点击选择记录日期:
InkWell(
onTap: () => _selectDate(context, true),
child: InputDecorator(
decoration: const InputDecoration(
labelText: '日期 *',
border: OutlineInputBorder(),
prefixIcon: Icon(Icons.calendar_today),
),
child: Text(DateFormat('yyyy-MM-dd').format(_date)),
),
),
SizedBox(height: 16.h),
InkWell包裹让整个区域可点击。
InputDecorator让显示样式与其他输入框一致。
医院信息输入
可选的医院字段:
TextFormField(
controller: _hospitalController,
decoration: const InputDecoration(
labelText: '医院 (选填)',
border: OutlineInputBorder(),
prefixIcon: Icon(Icons.local_hospital),
),
),
SizedBox(height: 16.h),
标签注明选填,用户知道这不是必填项。
医院图标让字段含义更直观。
医生信息输入
可选的医生字段:
TextFormField(
controller: _doctorController,
decoration: const InputDecoration(
labelText: '医生 (选填)',
border: OutlineInputBorder(),
prefixIcon: Icon(Icons.person),
),
),
SizedBox(height: 16.h),
记录医生信息方便下次就诊时参考。
这些可选字段让记录更加完整。
费用输入
带单位的费用输入框:
TextFormField(
controller: _costController,
keyboardType: TextInputType.number,
decoration: const InputDecoration(
labelText: '费用 (选填)',
border: OutlineInputBorder(),
prefixIcon: Icon(Icons.attach_money),
suffixText: '元',
),
),
SizedBox(height: 16.h),
keyboardType设为number弹出数字键盘。
suffixText显示单位,用户知道要输入的是金额。
下次日期选择
设置下次提醒日期:
InkWell(
onTap: () => _selectDate(context, false),
child: InputDecorator(
decoration: const InputDecoration(
labelText: '下次日期 (选填)',
border: OutlineInputBorder(),
prefixIcon: Icon(Icons.event),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(_nextDate != null ? DateFormat('yyyy-MM-dd').format(_nextDate!) : '未设置'),
if (_nextDate != null)
IconButton(
icon: const Icon(Icons.clear),
onPressed: () => setState(() => _nextDate = null),
),
],
),
),
),
SizedBox(height: 16.h),
下次日期用于提醒下次疫苗或驱虫时间。
清除按钮让用户可以取消已设置的日期。
详细描述输入
多行的描述输入框:
TextFormField(
controller: _descriptionController,
maxLines: 3,
decoration: const InputDecoration(
labelText: '详细描述 (选填)',
border: OutlineInputBorder(),
prefixIcon: Icon(Icons.description),
),
),
SizedBox(height: 32.h),
maxLines: 3让输入框有三行高度。
可以记录更详细的就诊情况。
保存按钮
底部的提交按钮:
SizedBox(
width: double.infinity,
height: 48.h,
child: ElevatedButton(
onPressed: _saveRecord,
child: const Text('保存'),
),
),
],
),
),
),
);
}
width: double.infinity让按钮撑满宽度。
点击触发_saveRecord方法保存数据。
类型转换方法
枚举转中文的方法:
String _getTypeString(HealthRecordType type) {
switch (type) {
case HealthRecordType.vaccination: return '疫苗接种';
case HealthRecordType.deworming: return '驱虫';
case HealthRecordType.checkup: return '体检';
case HealthRecordType.surgery: return '手术';
case HealthRecordType.medication: return '用药';
case HealthRecordType.other: return '其他';
}
}
switch语句处理每种类型的显示文本。
六种类型覆盖了常见的健康记录场景。
日期选择方法
弹出日期选择器:
Future<void> _selectDate(BuildContext context, bool isMainDate) async {
final picked = await showDatePicker(
context: context,
initialDate: isMainDate ? _date : (_nextDate ?? DateTime.now().add(const Duration(days: 30))),
firstDate: isMainDate ? DateTime(2000) : DateTime.now(),
lastDate: DateTime(2100),
);
if (picked != null) {
setState(() {
if (isMainDate) {
_date = picked;
} else {
_nextDate = picked;
}
});
}
}
isMainDate参数区分是选择记录日期还是下次日期。
下次日期默认是30天后,且不能选择过去的日期。
保存记录逻辑
验证并保存数据:
void _saveRecord() {
if (_formKey.currentState!.validate()) {
final record = HealthRecord(
catId: widget.catId,
type: _type,
title: _titleController.text,
description: _descriptionController.text.isEmpty ? null : _descriptionController.text,
date: _date,
hospital: _hospitalController.text.isEmpty ? null : _hospitalController.text,
doctor: _doctorController.text.isEmpty ? null : _doctorController.text,
cost: _costController.text.isEmpty ? null : double.tryParse(_costController.text),
nextDate: _nextDate,
);
context.read<HealthProvider>().addHealthRecord(record);
Navigator.pop(context);
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('健康记录添加成功!')),
);
}
}
}
validate触发表单验证,通过后才保存。
空字符串转为null,保持数据整洁。
数据模型设计
HealthRecord的结构:
class HealthRecord {
final String id;
final String catId;
final HealthRecordType type;
final String title;
final String? description;
final DateTime date;
final String? hospital;
final String? doctor;
final double? cost;
final DateTime? nextDate;
}
必填字段用普通类型,可选字段用问号标记。
这种设计让数据结构清晰明了。
表单验证要点
必填字段的验证:
validator: (value) => value?.isEmpty ?? true ? '请输入标题' : null,
返回字符串表示验证失败,显示错误信息。
返回null表示验证通过。
触发验证的方式:
if (_formKey.currentState!.validate()) {
// 验证通过,执行保存
}
validate方法会触发所有字段的验证。
只有全部通过才返回true。
InputDecorator使用
让非输入组件看起来像输入框:
InputDecorator(
decoration: const InputDecoration(
labelText: '日期 *',
border: OutlineInputBorder(),
prefixIcon: Icon(Icons.calendar_today),
),
child: Text(DateFormat('yyyy-MM-dd').format(_date)),
)
InputDecorator应用InputDecoration的样式。
内部可以放任何Widget,这里放的是Text。
可选字段处理
空字符串转null的技巧:
description: _descriptionController.text.isEmpty ? null : _descriptionController.text,
三元表达式判断是否为空。
空字符串存为null更合理。
费用字段的处理:
cost: _costController.text.isEmpty ? null : double.tryParse(_costController.text),
tryParse转换失败返回null,不会抛异常。
这样处理更安全。
小结
添加健康记录页面涉及的知识点比较多:
- 多种表单组件的使用
- 日期选择器的复用
- 可选字段的处理
- 表单验证和数据保存
这些技巧在其他表单页面也能用到,值得好好掌握。
欢迎加入OpenHarmony跨平台开发社区,一起交流Flutter开发经验:
https://openharmonycrossplatform.csdn.net
更多推荐


所有评论(0)