flutter_for_openharmony家庭药箱管理app实战+编辑药品实现
本文介绍了药品编辑功能的实现要点,主要包括数据初始化和页面结构设计。在数据初始化阶段,通过TextEditingController预填充现有药品信息,并对下拉选择字段进行有效性检查。页面采用Form组件管理表单验证,包含文本输入、下拉选择和日期选择器等复用组件。文本输入组件支持必填验证和多种键盘类型,下拉选择组件确保视觉统一,日期选择器则调用系统对话框方便操作。整体设计保持了与添加页面的一致性,

编辑药品功能让用户可以修改已录入的药品信息。当药品信息发生变化时,比如库存调整、有效期更新等,用户需要通过编辑功能来保持数据的准确性。
功能设计要点
编辑页面与添加页面类似,但需要预填充现有数据。用户可以修改任何字段,保存后更新到Provider中。页面需要处理数据初始化、表单验证和数据更新三个核心流程。
数据初始化
在initState中初始化所有控制器和状态:
void initState() {
super.initState();
_nameController = TextEditingController(text: widget.medicine.name);
_specificationController = TextEditingController(text: widget.medicine.specification);
_manufacturerController = TextEditingController(text: widget.medicine.manufacturer);
_quantityController = TextEditingController(text: widget.medicine.quantity.toString());
_usageController = TextEditingController(text: widget.medicine.usage);
_dosageController = TextEditingController(text: widget.medicine.dosage);
_sideEffectsController = TextEditingController(text: widget.medicine.sideEffects);
_contraindicationsController = TextEditingController(text: widget.medicine.contraindications);
_selectedCategory = _categories.contains(widget.medicine.category)
? widget.medicine.category
: '其他';
_selectedUnit = _units.contains(widget.medicine.unit)
? widget.medicine.unit
: '粒';
_selectedLocation = _locations.contains(widget.medicine.storageLocation)
? widget.medicine.storageLocation
: '其他';
_productionDate = widget.medicine.productionDate;
_expiryDate = widget.medicine.expiryDate;
}
每个TextEditingController在创建时传入初始值,这样输入框会显示现有数据。对于下拉选择字段,需要检查值是否在选项列表中,如果不在则使用默认值。这种处理避免了下拉框因为值不存在而报错。日期字段直接使用药品对象中的DateTime值。
页面结构
编辑页面的结构与添加页面基本一致:
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: [
_buildTextField(_nameController, '药品名称', required: true),
_buildDropdown('药品分类', _selectedCategory, _categories, (v) {
setState(() => _selectedCategory = v!);
}),
_buildTextField(_specificationController, '规格', required: true),
// 更多字段...
SizedBox(
width: double.infinity,
height: 48.h,
child: ElevatedButton(
onPressed: _updateMedicine,
child: Text('保存修改'),
),
),
],
),
),
),
);
}
使用Form组件管理表单验证,SingleChildScrollView支持滚动。所有输入字段垂直排列,底部是保存按钮。按钮文字改为"保存修改",明确告知用户这是编辑操作而非新增。
文本输入组件
封装的文本输入组件支持多种配置:
Widget _buildTextField(
TextEditingController controller,
String label, {
bool required = false,
TextInputType keyboardType = TextInputType.text,
int maxLines = 1,
}) {
return TextFormField(
controller: controller,
keyboardType: keyboardType,
maxLines: maxLines,
decoration: InputDecoration(
labelText: required ? '$label *' : label,
border: OutlineInputBorder(borderRadius: BorderRadius.circular(8.r)),
contentPadding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 12.h),
),
validator: required
? (v) => (v == null || v.isEmpty) ? '请输入$label' : null
: null,
);
}
这个组件与添加页面的完全相同,实现了代码复用。通过参数控制是否必填、键盘类型和最大行数。必填字段会在标签后添加星号,并在验证时检查是否为空。
下拉选择组件
下拉选择用于固定选项的字段:
Widget _buildDropdown(
String label,
String value,
List<String> items,
ValueChanged<String?> onChanged,
) {
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: items.map((i) => DropdownMenuItem(value: i, child: Text(i))).toList(),
onChanged: onChanged,
);
}
下拉框显示当前选中的值,用户点击后可以从列表中选择新值。选择变化时通过onChanged回调更新状态。装饰样式与文本输入框保持一致,确保视觉统一。
日期选择器
日期选择器调用系统日期选择对话框:
Widget _buildDatePicker(String label, DateTime date, ValueChanged<DateTime> onChanged) {
return GestureDetector(
onTap: () async {
final picked = await showDatePicker(
context: context,
initialDate: date,
firstDate: DateTime(2000),
lastDate: DateTime(2100),
);
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),
],
),
),
);
}
容器显示当前日期,点击时弹出日期选择器。initialDate设置为当前日期,用户可以在此基础上调整。选择完成后通过回调更新状态,界面自动刷新显示新日期。
数量和单位布局
数量和单位采用横向布局:
Row(
children: [
Expanded(
flex: 2,
child: _buildTextField(
_quantityController,
'数量',
keyboardType: TextInputType.number,
required: true,
),
),
SizedBox(width: 12.w),
Expanded(
child: _buildDropdown('单位', _selectedUnit, _units, (v) {
setState(() => _selectedUnit = v!);
}),
),
],
)
数量字段占2份空间,单位字段占1份空间。数量输入框使用数字键盘,方便用户输入。两个字段之间添加间距,避免视觉上的拥挤。
更新药品逻辑
保存时使用copyWith方法创建新对象:
void _updateMedicine() {
if (_formKey.currentState!.validate()) {
final updated = widget.medicine.copyWith(
name: _nameController.text,
category: _selectedCategory,
specification: _specificationController.text,
manufacturer: _manufacturerController.text,
productionDate: _productionDate,
expiryDate: _expiryDate,
quantity: int.tryParse(_quantityController.text) ?? 0,
unit: _selectedUnit,
storageLocation: _selectedLocation,
usage: _usageController.text,
dosage: _dosageController.text,
sideEffects: _sideEffectsController.text,
contraindications: _contraindicationsController.text,
);
context.read<MedicineProvider>().updateMedicine(updated);
Get.back();
Get.snackbar('成功', '药品信息已更新', snackPosition: SnackPosition.BOTTOM);
}
}
首先验证表单,确保必填字段都已填写。使用copyWith方法基于原对象创建新对象,只更新修改的字段,保持ID等不变。调用Provider的updateMedicine方法更新数据,Provider会通知所有监听者刷新UI。返回上一页并显示成功提示,让用户知道操作已完成。
copyWith方法的优势
Medicine模型中的copyWith方法让更新操作更加安全:
Medicine copyWith({
String? name,
String? category,
// 其他字段...
}) {
return Medicine(
id: id ?? this.id,
name: name ?? this.name,
category: category ?? this.category,
// 其他字段...
);
}
这个方法创建一个新的Medicine对象,只替换传入的字段,其他字段保持不变。这种不可变数据的设计模式让状态管理更加可靠,避免了直接修改对象可能带来的问题。
资源管理
在dispose方法中释放所有控制器:
void dispose() {
_nameController.dispose();
_specificationController.dispose();
_manufacturerController.dispose();
_quantityController.dispose();
_usageController.dispose();
_dosageController.dispose();
_sideEffectsController.dispose();
_contraindicationsController.dispose();
super.dispose();
}
每个TextEditingController都需要手动释放,这是Flutter的内存管理要求。忘记释放会导致内存泄漏,影响应用性能。
编辑与添加的区别
编辑页面与添加页面的主要区别在于数据初始化。编辑页面需要在initState中预填充数据,而添加页面使用空值或默认值。保存逻辑也不同:编辑使用updateMedicine方法,添加使用addMedicine方法。通过这种设计,两个页面可以共享大部分UI组件代码。
表单验证增强
为编辑表单添加更完善的验证逻辑:
String? _validateQuantity(String? value) {
if (value == null || value.isEmpty) {
return '请输入数量';
}
final quantity = int.tryParse(value);
if (quantity == null) {
return '请输入有效的数字';
}
if (quantity < 0) {
return '数量不能为负数';
}
if (quantity > 9999) {
return '数量不能超过9999';
}
return null;
}
String? _validateExpiryDate() {
if (_expiryDate.isBefore(_productionDate)) {
return '有效期不能早于生产日期';
}
return null;
}
数量验证检查是否为空、是否为有效数字、是否在合理范围内。日期验证确保有效期不早于生产日期。这些验证在用户提交前进行,避免保存无效数据。
变更检测功能
检测用户是否修改了数据,在返回时提示保存:
bool _hasChanges() {
return _nameController.text != widget.medicine.name ||
_selectedCategory != widget.medicine.category ||
_specificationController.text != widget.medicine.specification ||
_manufacturerController.text != widget.medicine.manufacturer ||
_productionDate != widget.medicine.productionDate ||
_expiryDate != widget.medicine.expiryDate ||
int.tryParse(_quantityController.text) != widget.medicine.quantity ||
_selectedUnit != widget.medicine.unit ||
_selectedLocation != widget.medicine.storageLocation;
}
Future<bool> _onWillPop() async {
if (!_hasChanges()) return true;
final result = await showDialog<bool>(
context: context,
builder: (context) => AlertDialog(
title: const Text('放弃修改?'),
content: const Text('您有未保存的修改,确定要放弃吗?'),
actions: [
TextButton(
onPressed: () => Navigator.pop(context, false),
child: const Text('继续编辑'),
),
TextButton(
onPressed: () => Navigator.pop(context, true),
child: const Text('放弃'),
),
],
),
);
return result ?? false;
}
_hasChanges方法比较当前值与原始值,判断是否有修改。使用WillPopScope包裹页面,在用户返回时检测变更。如果有未保存的修改,弹出确认对话框,防止用户意外丢失数据。
快速调整数量
为数量字段添加快速调整按钮:
Widget _buildQuantityField() {
return Row(
children: [
IconButton(
icon: const Icon(Icons.remove_circle_outline),
onPressed: () {
final current = int.tryParse(_quantityController.text) ?? 0;
if (current > 0) {
_quantityController.text = (current - 1).toString();
}
},
),
Expanded(
child: TextFormField(
controller: _quantityController,
keyboardType: TextInputType.number,
textAlign: TextAlign.center,
decoration: InputDecoration(
border: OutlineInputBorder(borderRadius: BorderRadius.circular(8.r)),
),
validator: _validateQuantity,
),
),
IconButton(
icon: const Icon(Icons.add_circle_outline),
onPressed: () {
final current = int.tryParse(_quantityController.text) ?? 0;
_quantityController.text = (current + 1).toString();
},
),
],
);
}
数量字段两侧添加加减按钮,用户可以快速调整数量而无需手动输入。这种设计特别适合库存盘点场景,提升了操作效率。
图片编辑功能
支持编辑药品图片:
String? _imagePath;
Widget _buildImagePicker() {
return GestureDetector(
onTap: _pickImage,
child: Container(
width: double.infinity,
height: 150.h,
decoration: BoxDecoration(
color: Colors.grey[100],
borderRadius: BorderRadius.circular(12.r),
border: Border.all(color: Colors.grey[300]!),
),
child: _imagePath != null
? ClipRRect(
borderRadius: BorderRadius.circular(12.r),
child: Image.file(File(_imagePath!), fit: BoxFit.cover),
)
: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.add_photo_alternate, size: 48.sp, color: Colors.grey[400]),
SizedBox(height: 8.h),
Text('点击添加药品图片', style: TextStyle(color: Colors.grey[500])),
],
),
),
);
}
Future<void> _pickImage() async {
final picker = ImagePicker();
final image = await picker.pickImage(source: ImageSource.gallery);
if (image != null) {
setState(() => _imagePath = image.path);
}
}
图片选择器显示当前图片或占位提示。点击后打开相册选择新图片。图片使用圆角裁剪,与卡片风格保持一致。
删除药品功能
在编辑页面提供删除药品的入口:
Widget _buildDeleteButton() {
return TextButton(
onPressed: _showDeleteConfirmation,
child: const Text('删除此药品', style: TextStyle(color: Colors.red)),
);
}
void _showDeleteConfirmation() {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('确认删除'),
content: Text('确定要删除"${widget.medicine.name}"吗?此操作无法撤销。'),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('取消'),
),
TextButton(
onPressed: () {
Navigator.pop(context);
context.read<MedicineProvider>().deleteMedicine(widget.medicine.id);
Get.back();
Get.snackbar('已删除', '药品已删除', snackPosition: SnackPosition.BOTTOM);
},
child: const Text('删除', style: TextStyle(color: Colors.red)),
),
],
),
);
}
删除按钮使用红色文字,放在页面底部。点击后弹出确认对话框,说明操作不可撤销。确认删除后返回上一页并显示提示。
历史记录功能
记录药品的修改历史:
class MedicineEditHistory {
final String medicineId;
final DateTime editTime;
final String editType;
final Map<String, dynamic> changes;
MedicineEditHistory({
required this.medicineId,
required this.editTime,
required this.editType,
required this.changes,
});
}
void _saveEditHistory(Medicine original, Medicine updated) {
final changes = <String, dynamic>{};
if (original.name != updated.name) {
changes['name'] = {'from': original.name, 'to': updated.name};
}
if (original.quantity != updated.quantity) {
changes['quantity'] = {'from': original.quantity, 'to': updated.quantity};
}
// 记录其他字段变更...
if (changes.isNotEmpty) {
final history = MedicineEditHistory(
medicineId: original.id,
editTime: DateTime.now(),
editType: 'update',
changes: changes,
);
// 保存历史记录
}
}
修改历史记录每次编辑的时间和变更内容。用户可以查看药品的修改历史,了解数据的变化过程。这对于追踪库存变化特别有用。
总结
编辑药品页面通过预填充数据和copyWith方法,实现了安全可靠的数据更新功能。表单验证确保数据完整性,Provider管理状态更新,用户体验流畅自然。
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
更多推荐



所有评论(0)