flutter_for_openharmony家庭药箱管理app实战+添加成员实现
本文介绍了家庭成员健康信息管理系统的表单设计与实现。系统采用分组表单结构,包含基本信息、身体状况、过敏史等5个部分。关键功能包括: 表单状态管理:使用多个控制器管理文本输入,列表存储过敏源和慢性病数据 交互组件实现: 性别单选按钮组 标签式过敏/疾病输入组件(支持添加和删除) 血型可选下拉框 布局优化:采用横向排列节省空间,如身高体重并排显示 系统通过精细的表单设计,实现了家庭成员健康信息的完整采

添加家庭成员是建立个性化健康管理的第一步。每个成员都有独特的健康信息,包括过敏史、慢性病、血型等,这些信息对于安全用药至关重要。
表单设计思路
添加成员页面采用分组表单设计,包含基本信息、身体信息、过敏信息、慢性病和其他信息五个部分。使用标签输入组件管理过敏源和慢性病列表,提供灵活的数据录入方式。
状态管理
页面状态包含多个控制器和变量:
class _AddFamilyMemberScreenState extends State<AddFamilyMemberScreen> {
final _formKey = GlobalKey<FormState>();
final _nameController = TextEditingController();
final _heightController = TextEditingController();
final _weightController = TextEditingController();
final _emergencyContactController = TextEditingController();
final _notesController = TextEditingController();
String _relationship = '本人';
String _gender = '男';
String? _bloodType;
DateTime _birthday = DateTime(1990, 1, 1);
List<String> _allergies = [];
List<String> _chronicDiseases = [];
final _allergyController = TextEditingController();
final _diseaseController = TextEditingController();
final List<String> _relationships = ['本人', '配偶', '子女', '父亲', '母亲', '祖父', '祖母', '其他'];
final List<String> _bloodTypes = ['A型', 'B型', 'AB型', 'O型'];
}
使用多个TextEditingController管理文本输入,String变量管理下拉选择,DateTime管理日期,List管理标签列表。预定义关系和血型选项,方便用户选择。
性别选择器
性别使用单选按钮组:
Widget _buildGenderSelector() {
return Container(
padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 8.h),
decoration: BoxDecoration(
border: Border.all(color: Colors.grey),
borderRadius: BorderRadius.circular(8.r),
),
child: Row(
children: [
Text('性别: ', style: TextStyle(fontSize: 14.sp)),
Expanded(
child: Row(
children: [
Radio<String>(
value: '男',
groupValue: _gender,
onChanged: (v) => setState(() => _gender = v!),
),
const Text('男'),
Radio<String>(
value: '女',
groupValue: _gender,
onChanged: (v) => setState(() => _gender = v!),
),
const Text('女'),
],
),
),
],
),
);
}
使用Radio组件创建单选按钮,groupValue设置当前选中值,onChanged回调更新状态。容器样式模仿输入框,保持视觉统一。
标签输入组件
过敏和慢性病使用标签输入:
Widget _buildTagInput(TextEditingController controller, List<String> tags, String hint) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Expanded(
child: TextField(
controller: controller,
decoration: InputDecoration(
hintText: hint,
border: OutlineInputBorder(borderRadius: BorderRadius.circular(8.r)),
contentPadding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 12.h),
),
),
),
SizedBox(width: 8.w),
IconButton(
onPressed: () {
if (controller.text.isNotEmpty) {
setState(() {
tags.add(controller.text);
controller.clear();
});
}
},
icon: Container(
padding: EdgeInsets.all(8.w),
decoration: BoxDecoration(
color: const Color(0xFF00897B),
borderRadius: BorderRadius.circular(8.r),
),
child: const Icon(Icons.add, color: Colors.white),
),
),
],
),
if (tags.isNotEmpty) ...[
SizedBox(height: 8.h),
Wrap(
spacing: 8.w,
runSpacing: 8.h,
children: tags.map((tag) {
return Container(
padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 6.h),
decoration: BoxDecoration(
color: Colors.teal.withOpacity(0.1),
borderRadius: BorderRadius.circular(20.r),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(tag, style: TextStyle(color: Colors.teal, fontSize: 12.sp)),
SizedBox(width: 4.w),
GestureDetector(
onTap: () => setState(() => tags.remove(tag)),
child: Icon(Icons.close, size: 16.sp, color: Colors.teal),
),
],
),
);
}).toList(),
),
],
],
);
}
输入框和添加按钮在同一行,用户输入后点击添加按钮将内容加入标签列表。已添加的标签显示在下方,每个标签右侧有删除按钮。使用Wrap组件自动换行,标签过多时不会超出屏幕。
身高体重布局
身高和体重采用横向布局:
Row(
children: [
Expanded(child: _buildTextField(_heightController, '身高(cm)', keyboardType: TextInputType.number)),
SizedBox(width: 12.w),
Expanded(child: _buildTextField(_weightController, '体重(kg)', keyboardType: TextInputType.number)),
],
)
两个输入框平均分配空间,都使用数字键盘。这种布局节省垂直空间,让表单更加紧凑。
血型下拉框
血型允许为空:
Widget _buildDropdown(
String label,
String? value,
List<String> items,
ValueChanged<String?> onChanged, {
bool allowNull = false,
}) {
return DropdownButtonFormField<String>(
value: value,
decoration: InputDecoration(
labelText: label,
border: OutlineInputBorder(borderRadius: BorderRadius.circular(8.r)),
contentPadding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 12.h),
),
items: [
if (allowNull) const DropdownMenuItem(value: null, child: Text('未知')),
...items.map((i) => DropdownMenuItem(value: i, child: Text(i))),
],
onChanged: onChanged,
);
}
allowNull参数控制是否允许空值。血型不是必填项,用户可以选择"未知"。这种设计提供了更大的灵活性。
保存成员逻辑
表单验证通过后创建成员对象:
void _saveMember() {
if (_formKey.currentState!.validate()) {
final member = FamilyMember(
id: const Uuid().v4(),
name: _nameController.text,
relationship: _relationship,
birthday: _birthday,
gender: _gender,
bloodType: _bloodType,
height: double.tryParse(_heightController.text),
weight: double.tryParse(_weightController.text),
allergies: _allergies,
chronicDiseases: _chronicDiseases,
emergencyContact: _emergencyContactController.text.isEmpty ? null : _emergencyContactController.text,
notes: _notesController.text.isEmpty ? null : _notesController.text,
);
context.read<FamilyProvider>().addMember(member);
Get.back();
Get.snackbar('成功', '家庭成员已添加', snackPosition: SnackPosition.BOTTOM);
}
}
使用double.tryParse转换身高体重,转换失败返回null。空字符串字段设置为null而非空字符串,保持数据的一致性。调用Provider的addMember方法保存数据,返回上一页并显示成功提示。
日期选择器
出生日期选择器限制日期范围:
Widget _buildDatePicker(String label, DateTime date, ValueChanged<DateTime> onChanged) {
return GestureDetector(
onTap: () async {
final picked = await showDatePicker(
context: context,
initialDate: date,
firstDate: DateTime(1900),
lastDate: DateTime.now(),
);
if (picked != null) onChanged(picked);
},
child: Container(
padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 16.h),
decoration: BoxDecoration(
border: Border.all(color: Colors.grey),
borderRadius: BorderRadius.circular(8.r),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text('$label: ${DateFormat('yyyy-MM-dd').format(date)}'),
const Icon(Icons.calendar_today, size: 20),
],
),
),
);
}
firstDate设置为1900年,lastDate设置为当前日期,确保出生日期在合理范围内。这种限制避免了用户输入错误的日期。
分组标题
每个信息组使用标题分隔:
Widget _buildSectionTitle(String title) {
return Padding(
padding: EdgeInsets.only(bottom: 12.h),
child: Text(title, style: TextStyle(fontSize: 16.sp, fontWeight: FontWeight.bold)),
);
}
标题使用加粗字体,底部添加间距。这种分组让长表单的结构更加清晰,用户能够快速定位到需要填写的部分。
资源释放
在dispose中释放所有控制器:
void dispose() {
_nameController.dispose();
_heightController.dispose();
_weightController.dispose();
_emergencyContactController.dispose();
_notesController.dispose();
_allergyController.dispose();
_diseaseController.dispose();
super.dispose();
}
每个TextEditingController都需要手动释放,这是Flutter的内存管理要求。
标签输入的优势
标签输入组件让用户可以添加任意数量的过敏源和慢性病。相比固定的输入框,这种方式更加灵活。用户可以根据实际情况添加多个标签,也可以随时删除不需要的标签。
总结
添加成员页面通过分组表单和标签输入,提供了灵活的数据录入方式。性别单选按钮、血型下拉框和日期选择器等组件,让表单填写更加便捷。Provider管理成员数据,确保数据的持久化和响应式更新。
表单验证增强
为表单添加更完善的验证逻辑:
String? _validateName(String? value) {
if (value == null || value.isEmpty) {
return '请输入姓名';
}
if (value.length < 2) {
return '姓名至少2个字符';
}
if (value.length > 20) {
return '姓名不能超过20个字符';
}
return null;
}
String? _validateHeight(String? value) {
if (value == null || value.isEmpty) return null;
final height = double.tryParse(value);
if (height == null) return '请输入有效的数字';
if (height < 30 || height > 250) return '身高范围应在30-250cm之间';
return null;
}
String? _validateWeight(String? value) {
if (value == null || value.isEmpty) return null;
final weight = double.tryParse(value);
if (weight == null) return '请输入有效的数字';
if (weight < 1 || weight > 500) return '体重范围应在1-500kg之间';
return null;
}
验证逻辑检查姓名长度、身高体重的合理范围。这些验证确保了数据的有效性,避免录入错误数据。
常用过敏源快速添加
提供常用过敏源的快速选择:
Widget _buildCommonAllergies() {
final commonAllergies = ['青霉素', '头孢', '磺胺', '阿司匹林', '花粉', '海鲜', '牛奶', '鸡蛋'];
return Wrap(
spacing: 8.w,
runSpacing: 8.h,
children: commonAllergies.map((allergy) {
final isSelected = _allergies.contains(allergy);
return GestureDetector(
onTap: () {
setState(() {
if (isSelected) {
_allergies.remove(allergy);
} else {
_allergies.add(allergy);
}
});
},
child: Container(
padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 6.h),
decoration: BoxDecoration(
color: isSelected ? Colors.red.withOpacity(0.1) : Colors.grey[200],
borderRadius: BorderRadius.circular(20.r),
border: isSelected ? Border.all(color: Colors.red) : null,
),
child: Text(
allergy,
style: TextStyle(
color: isSelected ? Colors.red : Colors.grey[700],
fontSize: 12.sp,
),
),
),
);
}).toList(),
);
}
常用过敏源以标签形式展示,用户点击即可添加或移除。这种设计比手动输入更加便捷,也减少了输入错误。
常用慢性病快速添加
同样提供常用慢性病的快速选择:
Widget _buildCommonDiseases() {
final commonDiseases = ['高血压', '糖尿病', '冠心病', '哮喘', '关节炎', '甲状腺疾病', '胃病', '肝病'];
return Wrap(
spacing: 8.w,
runSpacing: 8.h,
children: commonDiseases.map((disease) {
final isSelected = _chronicDiseases.contains(disease);
return GestureDetector(
onTap: () {
setState(() {
if (isSelected) {
_chronicDiseases.remove(disease);
} else {
_chronicDiseases.add(disease);
}
});
},
child: Container(
padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 6.h),
decoration: BoxDecoration(
color: isSelected ? Colors.orange.withOpacity(0.1) : Colors.grey[200],
borderRadius: BorderRadius.circular(20.r),
border: isSelected ? Border.all(color: Colors.orange) : null,
),
child: Text(
disease,
style: TextStyle(
color: isSelected ? Colors.orange : Colors.grey[700],
fontSize: 12.sp,
),
),
),
);
}).toList(),
);
}
慢性病标签使用橙色,与过敏源的红色形成区分。用户可以快速选择常见的慢性病,也可以通过输入框添加其他疾病。
头像选择功能
支持为成员设置头像:
String? _avatarPath;
Widget _buildAvatarPicker() {
return GestureDetector(
onTap: _pickAvatar,
child: Center(
child: Stack(
children: [
CircleAvatar(
radius: 50.r,
backgroundColor: Colors.teal.withOpacity(0.1),
backgroundImage: _avatarPath != null ? FileImage(File(_avatarPath!)) : null,
child: _avatarPath == null
? Icon(Icons.person, size: 50.sp, color: Colors.teal)
: null,
),
Positioned(
bottom: 0,
right: 0,
child: Container(
padding: EdgeInsets.all(4.w),
decoration: const BoxDecoration(
color: Color(0xFF00897B),
shape: BoxShape.circle,
),
child: Icon(Icons.camera_alt, size: 16.sp, color: Colors.white),
),
),
],
),
),
);
}
Future<void> _pickAvatar() async {
final picker = ImagePicker();
final image = await picker.pickImage(source: ImageSource.gallery);
if (image != null) {
setState(() => _avatarPath = image.path);
}
}
头像选择器显示当前头像或默认图标,右下角有相机图标提示可以更换。点击后打开相册选择图片。
紧急联系人验证
验证紧急联系人电话格式:
String? _validateEmergencyContact(String? value) {
if (value == null || value.isEmpty) return null;
final phoneRegex = RegExp(r'^1[3-9]\d{9}$');
if (!phoneRegex.hasMatch(value)) {
return '请输入有效的手机号码';
}
return null;
}
紧急联系人电话使用正则表达式验证格式,确保输入的是有效的手机号码。
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
更多推荐
所有评论(0)