作为一名长期从事医疗健康类应用开发的工程师,我最近使用Flutter框架完成了一款药品信息查询应用的开发。这款应用不仅具备完整的药品信息展示功能,还特别注重用药安全提示和用户体验设计。在华为鸿蒙系统上运行表现优异,充分展现了Flutter框架优秀的跨平台能力。
应用核心功能包括六大药品分类浏览、多维度搜索、收藏管理以及详细的用药安全提示。UI方面采用Material Design 3设计规范,配合精美的渐变效果和直观的信息展示方式,让用户在查询药品信息时获得清晰、舒适的体验。
提示:本教程假设读者已具备基础的Flutter开发知识,包括Widget使用、状态管理和基础网络请求等。若需要补充这些基础知识,建议先查阅Flutter官方文档。
应用采用典型的MVC架构模式,分为数据层、业务逻辑层和表现层:
code复制lib/
├── models/ # 数据模型
│ └── medicine.dart
├── services/ # 业务逻辑
│ └── medicine_service.dart
├── views/ # 页面组件
│ ├── home_page.dart
│ ├── search_page.dart
│ ├── favorites_page.dart
│ └── detail_page.dart
└── widgets/ # 公用组件
├── medicine_card.dart
└── info_chip.dart
这种架构设计使得代码职责清晰,便于后期维护和功能扩展。特别是在需要接入真实药品数据API时,只需修改service层即可,不会影响其他部分的代码。
应用包含以下主要页面及其交互关系:
页面导航使用Flutter标准的Navigator进行管理,底部采用NavigationBar组件实现主要功能切换。这种设计符合用户直觉,操作路径清晰。
药品模型(Medicine)是整个应用的核心数据结构,包含了药品的各类信息:
dart复制class Medicine {
final int id; // 药品唯一标识
final String name; // 商品名称(如"阿莫西林胶囊")
final String genericName; // 通用名称(如"阿莫西林")
final String category; // 分类(感冒药/消炎药等)
final String manufacturer; // 生产企业
final String dosageForm; // 剂型(片剂/胶囊等)
final String specification; // 规格(如"0.25g×24片")
final String indication; // 适应症
final String usage; // 用法用量
final String sideEffects; // 不良反应
final String contraindications; // 禁忌
final String precautions; // 注意事项
final double price; // 价格
final bool isPrescription; // 是否处方药
final String storage; // 贮藏条件
// 构造函数...
// fromJson/toJson方法...
}
设计这个模型时,我参考了国家药品标准信息格式,确保包含用药安全所需的所有关键信息。特别是将药品名称和通用名分开存储,便于后续的搜索功能实现。
应用将药品分为六大类,每类包含典型药品示例:
| 分类 | 示例药品 | 主要用途 |
|---|---|---|
| 感冒药 | 感冒灵颗粒 | 缓解感冒症状 |
| 消炎药 | 阿莫西林、头孢克肟 | 抗菌消炎 |
| 止痛药 | 布洛芬 | 缓解疼痛 |
| 胃药 | 奥美拉唑 | 治疗胃病 |
| 心血管药 | 阿司匹林 | 心血管疾病 |
| 降压药 | 硝苯地平 | 降低血压 |
这种分类方式既符合大众认知习惯,又涵盖了常见药品类型。在实际开发中,可以根据需要扩展更多分类。
药品剂型也是重要信息之一,应用中定义了五种常见剂型:
dart复制final dosageForms = ['片剂', '胶囊', '颗粒', '口服液', '注射液'];
不同剂型对应不同的使用方法和注意事项。例如:
在开发初期,我们使用本地生成的模拟数据进行开发和测试:
dart复制static List<Medicine> _generateMedicines() {
final random = Random(42); // 固定种子保证数据一致
final manufacturers = ['华北制药', '华东医药', '华南药业', '西部生物'];
final medicineData = [
{
'name': '阿莫西林胶囊',
'generic': '阿莫西林',
'category': '消炎药',
'indication': '用于敏感菌所致的呼吸道、尿路等感染'
},
// 更多药品数据...
];
return medicineData.map((data) => Medicine(
id: medicineData.indexOf(data) + 1,
name: data['name']!,
genericName: data['generic']!,
category: data['category']!,
manufacturer: '${manufacturers[random.nextInt(4)]}有限公司',
dosageForm: dosageForms[random.nextInt(dosageForms.length)],
specification: '${[10, 20, 50, 100][random.nextInt(4)]}mg×${[12, 24, 30][random.nextInt(3)]}片',
indication: data['indication']!,
usage: '口服。成人一次1-2片,一日3次,饭后服用。',
sideEffects: '可能出现恶心、呕吐、腹泻、皮疹等不良反应。',
contraindications: '对青霉素类过敏者禁用。',
precautions: '用药期间避免饮酒。',
price: (random.nextInt(50) + 10).toDouble(),
isPrescription: random.nextBool(),
storage: '密封,置阴凉干燥处保存。',
)).toList();
}
注意:实际应用中应该使用真实药品数据源。这里的模拟数据仅用于开发测试,正式上线前需要替换为权威数据。
应用提供了强大的药品筛选和搜索功能:
dart复制List<Medicine> get _filteredMedicines {
var filtered = _medicines;
// 分类筛选
if (_selectedCategory != '全部') {
filtered = filtered.where((m) => m.category == _selectedCategory).toList();
}
// 关键词搜索
if (_searchQuery.isNotEmpty) {
filtered = filtered.where((m) {
return m.name.toLowerCase().contains(_searchQuery.toLowerCase()) ||
m.genericName.toLowerCase().contains(_searchQuery.toLowerCase()) ||
m.indication.toLowerCase().contains(_searchQuery.toLowerCase());
}).toList();
}
return filtered;
}
搜索功能特点:
药品列表使用卡片式设计,关键信息一目了然:
dart复制Widget _buildMedicineCard(Medicine medicine) {
return Card(
child: InkWell(
onTap: () => _navigateToDetail(medicine),
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
children: [
// 头部:图标、名称、处方标识
Row(
children: [
_buildMedicineIcon(),
Expanded(child: _buildNameSection(medicine)),
if (medicine.isPrescription) _buildPrescriptionTag(),
],
),
// 分类和生产企业
Row(
children: [
_buildInfoChip(Icons.category, medicine.category, Colors.blue),
_buildInfoChip(Icons.business, medicine.manufacturer, Colors.orange),
],
),
// 适应症摘要
Text(medicine.indication, maxLines: 2, overflow: TextOverflow.ellipsis),
// 价格和规格
_buildPriceAndSpec(medicine),
],
),
),
),
);
}
卡片设计要点:
首页采用渐变标题栏设计,视觉效果突出:
dart复制Widget _buildHeader() {
return Container(
padding: const EdgeInsets.fromLTRB(16, 48, 16, 16),
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [Colors.teal.shade600, Colors.teal.shade400],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
),
child: Row(
children: [
Icon(Icons.local_pharmacy, color: Colors.white, size: 32),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('药品信息查询', style: TextStyle(color: Colors.white, fontSize: 18)),
Text('安全用药,健康生活', style: TextStyle(color: Colors.white70, fontSize: 14)),
],
),
Spacer(),
Container(
padding: EdgeInsets.symmetric(horizontal: 12, vertical: 6),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.2),
borderRadius: BorderRadius.circular(16),
),
child: Text('${_medicines.length}种药品', style: TextStyle(color: Colors.white)),
),
],
),
);
}
首页还包含分类标签栏,使用水平滚动ListView实现:
dart复制Widget _buildCategoryTabBar() {
return SizedBox(
height: 50,
child: ListView.builder(
scrollDirection: Axis.horizontal,
itemCount: categories.length,
itemBuilder: (context, index) {
final category = categories[index];
final isSelected = category == _selectedCategory;
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 8),
child: ChoiceChip(
label: Text(category),
selected: isSelected,
onSelected: (_) => setState(() => _selectedCategory = category),
selectedColor: Colors.teal,
labelStyle: TextStyle(color: isSelected ? Colors.white : Colors.black),
),
);
},
),
);
}
搜索页提供实时搜索功能,用户体验流畅:
dart复制Widget _buildSearchPage() {
return Column(
children: [
// 搜索头部
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [Colors.teal.shade600, Colors.teal.shade400],
),
),
child: Column(
children: [
Text('搜索药品', style: TextStyle(color: Colors.white, fontSize: 20)),
SizedBox(height: 8),
TextField(
controller: _searchController,
decoration: InputDecoration(
hintText: '输入药品名称、通用名或适应症',
prefixIcon: Icon(Icons.search, color: Colors.teal),
suffixIcon: _searchQuery.isNotEmpty
? IconButton(
icon: Icon(Icons.clear, color: Colors.teal),
onPressed: _clearSearch,
)
: null,
filled: true,
fillColor: Colors.white,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(24),
borderSide: BorderSide.none,
),
),
onChanged: (value) => setState(() => _searchQuery = value),
),
],
),
),
// 搜索结果
Expanded(
child: _buildSearchResults(),
),
],
);
}
搜索功能特点:
详情页完整展示药品信息,分为多个信息区块:
dart复制class MedicineDetailPage extends StatelessWidget {
final Medicine medicine;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('药品详情'),
actions: [
IconButton(icon: Icon(Icons.favorite_border), onPressed: _toggleFavorite),
IconButton(icon: Icon(Icons.share), onPressed: _shareMedicine),
],
),
body: SingleChildScrollView(
child: Column(
children: [
_buildHeader(),
SizedBox(height: 16),
_buildBasicInfoCard(),
_buildUsageCard(),
_buildSafetyCard(),
_buildStorageCard(),
],
),
),
);
}
Widget _buildHeader() {
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [Colors.teal.shade600, Colors.teal.shade400],
),
),
child: Column(
children: [
Row(
children: [
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.2),
borderRadius: BorderRadius.circular(12),
),
child: Icon(Icons.medication, color: Colors.white, size: 40),
),
SizedBox(width: 16),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(medicine.name, style: TextStyle(color: Colors.white, fontSize: 20)),
Text(medicine.genericName, style: TextStyle(color: Colors.white70)),
],
),
],
),
SizedBox(height: 16),
Row(
children: [
if (medicine.isPrescription)
Container(
padding: EdgeInsets.symmetric(horizontal: 12, vertical: 6),
decoration: BoxDecoration(
color: Colors.red.withOpacity(0.2),
borderRadius: BorderRadius.circular(12),
),
child: Text('处方药', style: TextStyle(color: Colors.white)),
),
SizedBox(width: 8),
Container(
padding: EdgeInsets.symmetric(horizontal: 12, vertical: 6),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.2),
borderRadius: BorderRadius.circular(12),
),
child: Text(medicine.category, style: TextStyle(color: Colors.white)),
),
Spacer(),
Text('¥${medicine.price.toStringAsFixed(2)}',
style: TextStyle(color: Colors.white, fontSize: 24)),
],
),
],
),
);
}
// 其他信息卡片...
}
详情页设计要点:
实际应用中应该接入权威药品数据源,例如:
dart复制class MedicineService {
static const String baseUrl = 'https://api.example.com/medicines';
Future<List<Medicine>> searchMedicines(String keyword) async {
try {
final response = await http.get(
Uri.parse('$baseUrl/search?keyword=$keyword'),
headers: {'Authorization': 'Bearer $apiKey'},
);
if (response.statusCode == 200) {
final data = jsonDecode(response.body) as List;
return data.map((json) => Medicine.fromJson(json)).toList();
}
throw Exception('搜索失败: ${response.statusCode}');
} catch (e) {
throw Exception('网络请求失败: $e');
}
}
// 其他API方法...
}
API接入注意事项:
使用mobile_scanner插件实现药品条码扫描:
dart复制class BarcodeScannerPage extends StatefulWidget {
@override
_BarcodeScannerPageState createState() => _BarcodeScannerPageState();
}
class _BarcodeScannerPageState extends State<BarcodeScannerPage> {
final MobileScannerController controller = MobileScannerController();
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('扫码查询')),
body: Stack(
children: [
MobileScanner(
controller: controller,
onDetect: (barcode) {
final String? code = barcode.rawValue;
if (code != null) {
Navigator.pop(context);
_searchByBarcode(code);
}
},
),
_buildScannerOverlay(),
],
),
);
}
Future<void> _searchByBarcode(String code) async {
try {
final medicine = await MedicineService().getByBarcode(code);
Navigator.push(context, MaterialPageRoute(
builder: (context) => MedicineDetailPage(medicine: medicine),
));
} catch (e) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('未找到对应药品: $e')),
);
}
}
}
扫码功能优化点:
使用flutter_local_notifications实现定时提醒:
dart复制class MedicationReminder {
static final _notifications = FlutterLocalNotificationsPlugin();
static Future<void> init() async {
const android = AndroidInitializationSettings('@mipmap/ic_launcher');
const ios = DarwinInitializationSettings();
await _notifications.initialize(
InitializationSettings(android: android, iOS: ios),
onDidReceiveNotificationResponse: _onNotificationClicked,
);
}
static Future<void> scheduleReminder({
required int id,
required String medicineName,
required TimeOfDay time,
required List<int> weekdays,
}) async {
await _notifications.zonedSchedule(
id,
'用药提醒',
'该服用 $medicineName 了',
_nextInstanceOfTime(time, weekdays),
const NotificationDetails(
android: AndroidNotificationDetails(
'medication_reminder',
'用药提醒',
importance: Importance.high,
priority: Priority.high,
sound: RawResourceAndroidNotificationSound('notification'),
),
),
uiLocalNotificationDateInterpretation:
UILocalNotificationDateInterpretation.absoluteTime,
androidScheduleMode: AndroidScheduleMode.exactAllowWhileIdle,
matchDateTimeComponents: DateTimeComponents.time,
);
}
static TZDateTime _nextInstanceOfTime(TimeOfDay time, List<int> weekdays) {
// 计算下一次提醒时间...
}
static void _onNotificationClicked(NotificationResponse response) {
// 处理通知点击...
}
}
提醒功能注意事项:
在鸿蒙系统上运行时,我们可以利用一些系统特有功能增强用户体验:
在鸿蒙系统上运行Flutter应用时,可以采取以下优化措施:
通过platform channels调用鸿蒙系统API:
dart复制class HarmonyOSUtils {
static const MethodChannel _channel = MethodChannel('com.example/harmonyos');
static Future<void> addToServiceCenter(String medicineName) async {
try {
await _channel.invokeMethod('addToServiceCenter', {
'title': '$medicineName 药品信息',
'description': '快速查询药品详情',
'icon': 'medicine_icon',
});
} catch (e) {
debugPrint('调用鸿蒙API失败: $e');
}
}
// 其他鸿蒙API封装...
}
在开发这款药品信息查询应用的过程中,我积累了一些有价值的经验:
数据准确性至关重要:医疗健康类应用对数据准确性要求极高,必须使用权威数据源,并定期更新。
用户体验细节:药品信息的展示方式直接影响用户理解,需要精心设计信息层级和视觉提示。
性能与功能平衡:在保证功能完整性的同时,要注意应用性能,特别是在低端设备上的表现。
多平台适配:Flutter虽然具有良好的跨平台能力,但仍需针对不同平台(如鸿蒙)进行特定优化。
安全合规:医疗类应用涉及用户健康数据,必须严格遵守相关法律法规,做好数据保护。
这个项目充分展示了Flutter框架在医疗健康领域的应用潜力,特别是在需要快速开发、多平台部署的场景下。通过合理的设计和优化,Flutter应用完全能够满足专业医疗信息查询的需求。