flutter_for_openharmony家庭药箱管理app实战+添加药品实现
药箱管理应用的添加药品表单设计采用分组布局,分为基本信息、库存信息等5个模块,提升填写效率。使用Flutter框架实现,通过Form组件管理表单验证,封装通用输入组件(文本输入、下拉选择、日期选择)确保UI一致性。状态管理采用TextEditingController和状态变量,预定义药品分类、单位等选项列表,避免用户输入错误。整体设计注重用户体验,包括必填字段验证、友好的错误提示和统一的视觉样式

添加药品是药箱管理应用的基础功能。用户需要录入药品的各种信息,包括名称、规格、有效期、库存等。一个设计良好的表单能够让用户快速完成药品录入,同时确保数据的完整性和准确性。
表单设计思路
添加药品页面采用分组表单的设计方式,将相关信息归类展示:基本信息、库存信息、有效期、用法用量和其他信息。这种分组方式让表单结构清晰,用户填写时不会感到混乱。
页面状态管理
首先定义页面的状态和控制器:
class _AddMedicineScreenState extends State<AddMedicineScreen> {
final _formKey = GlobalKey<FormState>();
final _nameController = TextEditingController();
final _specificationController = TextEditingController();
final _manufacturerController = TextEditingController();
final _quantityController = TextEditingController();
String _selectedCategory = '感冒用药';
String _selectedUnit = '粒';
String _selectedLocation = '客厅药箱';
DateTime _productionDate = DateTime.now();
DateTime _expiryDate = DateTime.now().add(const Duration(days: 365));
final List<String> _categories = [
'感冒用药', '解热镇痛', '抗生素', '消化系统',
'心血管', '维生素', '外用药', '中成药', '儿童用药', '其他',
];
final List<String> _units = ['粒', '片', '袋', '支', '瓶', '盒', 'ml', 'g'];
final List<String> _locations = ['客厅药箱', '卧室药箱', '厨房药箱', '冰箱', '其他'];
}
使用GlobalKey<FormState>管理表单验证。为每个输入字段创建独立的TextEditingController,方便获取和管理输入内容。下拉选择字段使用String类型的状态变量,日期字段使用DateTime类型。预定义了分类、单位和存放位置的选项列表,用户可以从中选择,避免输入错误。
表单整体结构
页面主体使用Form包裹所有输入组件:
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: [
_buildSectionTitle('基本信息'),
_buildTextField(
controller: _nameController,
label: '药品名称',
hint: '请输入药品名称',
required: true,
),
_buildDropdown(
label: '药品分类',
value: _selectedCategory,
items: _categories,
onChanged: (value) {
setState(() {
_selectedCategory = value!;
});
},
),
// 更多字段...
SizedBox(
width: double.infinity,
height: 48.h,
child: ElevatedButton(
onPressed: _saveMedicine,
child: Text('保存'),
),
),
],
),
),
),
);
}
使用SingleChildScrollView让表单可以滚动,因为字段较多可能超出屏幕高度。所有输入组件通过Column垂直排列,crossAxisAlignment.start确保左对齐。底部的保存按钮宽度设置为double.infinity,占满整行,高度48.h提供足够的点击区域。
文本输入字段
封装通用的文本输入组件:
Widget _buildTextField({
required TextEditingController controller,
required String label,
String? hint,
bool required = false,
TextInputType keyboardType = TextInputType.text,
int maxLines = 1,
}) {
return TextFormField(
controller: controller,
keyboardType: keyboardType,
maxLines: maxLines,
decoration: InputDecoration(
labelText: required ? '$label *' : label,
hintText: hint,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8.r),
),
contentPadding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 12.h),
),
validator: required
? (value) {
if (value == null || value.isEmpty) {
return '请输入$label';
}
return null;
}
: null,
);
}
这个封装的组件支持多种配置:必填字段会在标签后添加星号,可以设置键盘类型(数字、文本等),支持多行输入。validator参数控制是否进行必填验证,验证失败时显示友好的错误提示。使用OutlineInputBorder提供边框样式,圆角8.r让输入框更加柔和。
下拉选择组件
下拉选择用于分类、单位等固定选项:
Widget _buildDropdown({
required String label,
required String value,
required List<String> items,
required 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((item) {
return DropdownMenuItem(value: item, child: Text(item));
}).toList(),
onChanged: onChanged,
);
}
DropdownButtonFormField是Flutter提供的下拉选择组件,与Form表单集成良好。通过items参数传入选项列表,value参数设置当前选中值,onChanged回调处理选择变化。装饰样式与文本输入框保持一致,确保视觉统一。
日期选择器
日期选择用于生产日期和有效期:
Widget _buildDatePicker({
required String label,
required DateTime date,
required 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)}',
style: TextStyle(fontSize: 14.sp),
),
const Icon(Icons.calendar_today, size: 20),
],
),
),
);
}
使用GestureDetector包裹容器,点击时调用showDatePicker显示系统日期选择器。日期范围设置为2000年到2100年,覆盖了所有可能的药品日期。选择完成后通过onChanged回调更新状态。容器样式模仿输入框,右侧显示日历图标提示用户可以点击。使用DateFormat格式化日期显示为"年-月-日"格式。
分组标题
每个信息组使用标题分隔:
Widget _buildSectionTitle(String title) {
return Padding(
padding: EdgeInsets.only(bottom: 12.h),
child: Text(
title,
style: TextStyle(
fontSize: 16.sp,
fontWeight: FontWeight.bold,
),
),
);
}
标题使用16.sp的加粗字体,比普通文本更醒目。底部添加12.h的间距,与下方的输入字段分隔开。这种分组设计让长表单的结构更加清晰,用户能够快速定位到需要填写的部分。
库存信息布局
数量和单位字段采用横向布局:
Row(
children: [
Expanded(
flex: 2,
child: _buildTextField(
controller: _quantityController,
label: '数量',
hint: '请输入数量',
keyboardType: TextInputType.number,
required: true,
),
),
SizedBox(width: 12.w),
Expanded(
child: _buildDropdown(
label: '单位',
value: _selectedUnit,
items: _units,
onChanged: (value) {
setState(() {
_selectedUnit = value!;
});
},
),
),
],
)
使用Row将数量和单位放在同一行,数量字段占2份空间,单位字段占1份空间,这样的比例让布局更加合理。数量输入框设置keyboardType为数字键盘,方便用户输入。两个字段之间添加12.w的间距,避免紧贴在一起。
保存药品逻辑
表单验证通过后创建药品对象并保存:
void _saveMedicine() {
if (_formKey.currentState!.validate()) {
final medicine = Medicine(
id: const Uuid().v4(),
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,
barcode: _barcodeController.text.isEmpty ? null : _barcodeController.text,
);
context.read<MedicineProvider>().addMedicine(medicine);
Get.back();
Get.snackbar('成功', '药品添加成功', snackPosition: SnackPosition.BOTTOM);
}
}
首先调用_formKey.currentState!.validate()验证所有必填字段。验证通过后,从各个控制器和状态变量中获取数据,创建Medicine对象。使用Uuid().v4()生成唯一ID。数量字段使用int.tryParse转换,如果转换失败默认为0。条形码字段为空时设置为null。
数据保存流程:通过context.read<MedicineProvider>()获取Provider实例,调用addMedicine方法添加药品。Provider会自动通知所有监听者更新UI。使用Get.back()返回上一页,Get.snackbar显示成功提示。这种反馈让用户明确知道操作已完成。
资源释放
在dispose方法中释放所有控制器:
void dispose() {
_nameController.dispose();
_specificationController.dispose();
_manufacturerController.dispose();
_quantityController.dispose();
_usageController.dispose();
_dosageController.dispose();
_sideEffectsController.dispose();
_contraindicationsController.dispose();
_barcodeController.dispose();
super.dispose();
}
TextEditingController使用完毕后必须释放,否则会造成内存泄漏。在dispose方法中依次调用每个控制器的dispose方法。这是Flutter开发的最佳实践,确保应用的内存使用效率。
表单验证机制
表单验证确保数据的完整性。必填字段通过validator参数进行验证,如果用户未填写,会在字段下方显示红色错误提示。只有所有验证通过,_formKey.currentState!.validate()才会返回true,保存操作才会执行。这种机制有效防止了无效数据的录入。
总结
添加药品页面通过分组表单的设计,让用户能够有条理地录入药品信息。封装的通用组件提高了代码复用性,表单验证确保了数据质量。使用Provider进行状态管理,实现了数据的响应式更新。
条形码扫描功能
支持通过扫描条形码快速录入药品信息:
Widget _buildBarcodeScanner() {
return GestureDetector(
onTap: _scanBarcode,
child: Container(
padding: EdgeInsets.all(12.w),
decoration: BoxDecoration(
color: const Color(0xFF00897B).withOpacity(0.1),
borderRadius: BorderRadius.circular(8.r),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.qr_code_scanner, color: const Color(0xFF00897B), size: 24.sp),
SizedBox(width: 8.w),
Text('扫描条形码', style: TextStyle(color: const Color(0xFF00897B), fontSize: 14.sp)),
],
),
),
);
}
Future<void> _scanBarcode() async {
final result = await BarcodeScanner.scan();
if (result.rawContent.isNotEmpty) {
setState(() {
_barcodeController.text = result.rawContent;
});
// 可以根据条形码查询药品信息
_lookupMedicineByBarcode(result.rawContent);
}
}
条形码扫描功能让用户可以快速录入药品信息,避免手动输入的繁琐。扫描后可以自动填充药品名称、规格等信息。
药品图片拍照
支持拍照记录药品外观:
String? _imagePath;
Widget _buildImagePicker() {
return GestureDetector(
onTap: _pickImage,
child: Container(
width: double.infinity,
height: 120.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: 40.sp, color: Colors.grey[400]),
SizedBox(height: 8.h),
Text('添加药品图片', style: TextStyle(color: Colors.grey[500], fontSize: 12.sp)),
],
),
),
);
}
Future<void> _pickImage() async {
final picker = ImagePicker();
final source = await showModalBottomSheet<ImageSource>(
context: context,
builder: (context) => Column(
mainAxisSize: MainAxisSize.min,
children: [
ListTile(
leading: const Icon(Icons.camera_alt),
title: const Text('拍照'),
onTap: () => Navigator.pop(context, ImageSource.camera),
),
ListTile(
leading: const Icon(Icons.photo_library),
title: const Text('从相册选择'),
onTap: () => Navigator.pop(context, ImageSource.gallery),
),
],
),
);
if (source != null) {
final image = await picker.pickImage(source: source);
if (image != null) {
setState(() => _imagePath = image.path);
}
}
}
图片功能让用户可以拍照记录药品外观,方便后续识别。支持拍照和从相册选择两种方式。
有效期提醒设置
添加药品时可以设置有效期提醒:
int _expiryWarningDays = 30;
Widget _buildExpiryWarningSelector() {
return Row(
children: [
Expanded(
child: Text('过期提前提醒', style: TextStyle(fontSize: 14.sp)),
),
DropdownButton<int>(
value: _expiryWarningDays,
items: [7, 14, 30, 60, 90].map((days) {
return DropdownMenuItem(value: days, child: Text('$days天'));
}).toList(),
onChanged: (v) => setState(() => _expiryWarningDays = v!),
),
],
);
}
有效期提醒设置让用户可以选择提前多少天收到过期提醒。默认30天,用户可以根据药品特性调整。
药品分类建议
根据药品名称自动建议分类:
void _suggestCategory(String name) {
final categoryKeywords = {
'感冒用药': ['感冒', '退烧', '发热', '咳嗽'],
'解热镇痛': ['布洛芬', '阿司匹林', '止痛'],
'抗生素': ['头孢', '阿莫西林', '青霉素'],
'消化系统': ['胃', '肠', '消化', '便秘'],
'心血管': ['降压', '心脏', '血压'],
'维生素': ['维生素', 'VC', 'VB', '钙片'],
};
for (final entry in categoryKeywords.entries) {
for (final keyword in entry.value) {
if (name.contains(keyword)) {
setState(() => _selectedCategory = entry.key);
return;
}
}
}
}
分类建议功能根据药品名称中的关键词自动推荐分类,减少用户的选择操作。
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
更多推荐

所有评论(0)