Flutter for OpenHarmony 实战:药物提醒 - 定时提醒服药,记录用药历史
在移动开发领域,我们总是面临着选择与适配。今天,你的Flutter应用在Android和iOS上跑得正欢,明天可能就需要考虑一个新的平台:HarmonyOS(鸿蒙)。这不是一道选答题,而是很多团队正在面对的现实。Flutter的优势很明确——写一套代码,就能在两个主要平台上运行,开发体验流畅。而鸿蒙代表的是下一个时代的互联生态,它不仅仅是手机系统,更着眼于未来全场景的体验。
Flutter for OpenHarmony 实战:药物提醒 - 定时提醒服药,记录用药历史
欢迎加入开源鸿蒙跨平台社区: https://openharmonycrossplatform.csdn.net
目录
前言:跨生态开发的新机遇
在移动开发领域,我们总是面临着选择与适配。今天,你的Flutter应用在Android和iOS上跑得正欢,明天可能就需要考虑一个新的平台:HarmonyOS(鸿蒙)。这不是一道选答题,而是很多团队正在面对的现实。
Flutter的优势很明确——写一套代码,就能在两个主要平台上运行,开发体验流畅。而鸿蒙代表的是下一个时代的互联生态,它不仅仅是手机系统,更着眼于未来全场景的体验。将现有的Flutter应用适配到鸿蒙,听起来像是一个“跨界”任务,但它本质上是一次有价值的技术拓展:让产品触达更多用户,也让技术栈覆盖更广。
不过,这条路走起来并不像听起来那么简单。Flutter和鸿蒙,从底层的架构到上层的工具链,都有着各自的设计逻辑。会遇到一些具体的问题:代码如何组织?原有的功能在鸿蒙上如何实现?那些平台特有的能力该怎么调用?更实际的是,从编译打包到上架部署,整个流程都需要重新摸索。
这篇文章想做的,就是把这些我们趟过的路、踩过的坑,清晰地摊开给你看。我们不会只停留在“怎么做”,还会聊到“为什么得这么做”,以及“如果出了问题该往哪想”。这更像是一份实战笔记,源自真实的项目经验,聚焦于那些真正卡住过我们的环节。
无论你是在为一个成熟产品寻找新的落地平台,还是从一开始就希望构建能面向多端的应用,这里的思路和解决方案都能提供直接的参考。理解了两套体系之间的异同,掌握了关键的衔接技术,不仅能完成这次迁移,更能积累起应对未来技术变化的能力。
混合工程结构深度解析
项目目录架构
当Flutter项目集成鸿蒙支持后,典型的项目结构会发生显著变化。以下是经过ohos_flutter插件初始化后的项目结构:
my_flutter_harmony_app/
├── lib/ # Flutter业务代码(基本不变)
│ ├── main.dart # 应用入口
│ ├── home_page.dart # 首页
│ └── utils/
│ └── platform_utils.dart # 平台工具类
├── pubspec.yaml # Flutter依赖配置
├── ohos/ # 鸿蒙原生层(核心适配区)
│ ├── entry/ # 主模块
│ │ └── src/main/
│ │ ├── ets/ # ArkTS代码
│ │ │ ├── MainAbility/
│ │ │ │ ├── MainAbility.ts # 主Ability
│ │ │ │ └── MainAbilityContext.ts
│ │ │ └── pages/
│ │ │ ├── Index.ets # 主页面
│ │ │ └── Splash.ets # 启动页
│ │ ├── resources/ # 鸿蒙资源文件
│ │ │ ├── base/
│ │ │ │ ├── element/ # 字符串等
│ │ │ │ ├── media/ # 图片资源
│ │ │ │ └── profile/ # 配置文件
│ │ │ └── en_US/ # 英文资源
│ │ └── config.json # 应用核心配置
│ ├── ohos_test/ # 测试模块
│ ├── build-profile.json5 # 构建配置
│ └── oh-package.json5 # 鸿蒙依赖管理
└── README.md
展示效果图片
flutter 实时预览 效果展示
运行到鸿蒙虚拟设备中效果展示
功能代码实现
应用入口实现
应用的入口文件 main.dart 负责初始化应用并加载主页面。在本项目中,我们直接在首页集成了药物提醒功能,无需额外的页面跳转。
import 'package:flutter/material.dart';
import 'components/medicine_reminder.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter for OpenHarmony',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
useMaterial3: true,
),
debugShowCheckedModeBanner: false,
home: const MyHomePage(title: 'Flutter for OpenHarmony'),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key, required this.title});
final String title;
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: const MedicineReminder(),
);
}
}
实现要点
- 使用
MaterialApp配置应用主题和标题 - 通过
Scaffold构建基本页面结构 - 直接在
body中引入MedicineReminder组件,实现首页直接展示
药物提醒主组件
medicine_reminder.dart 是整个功能的核心组件,负责管理药物列表、处理状态更新和展示用药统计信息。
import 'package:flutter/material.dart';
import 'medicine_item.dart';
class MedicineReminder extends StatefulWidget {
const MedicineReminder({super.key});
State<MedicineReminder> createState() => _MedicineReminderState();
}
class _MedicineReminderState extends State<MedicineReminder> {
List<Medicine> medicines = [
Medicine(
name: '阿莫西林',
dosage: '500mg',
time: '08:00',
taken: false,
icon: Icons.medication,
),
Medicine(
name: '布洛芬',
dosage: '200mg',
time: '12:00',
taken: false,
icon: Icons.medication,
),
Medicine(
name: '维生素C',
dosage: '100mg',
time: '18:00',
taken: false,
icon: Icons.medication_liquid,
),
];
void toggleMedicine(int index) {
setState(() {
medicines[index].taken = !medicines[index].taken;
});
}
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'今日用药提醒',
style: Theme.of(context).textTheme.headlineSmall?.copyWith(
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 16),
ListView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: medicines.length,
itemBuilder: (context, index) {
return MedicineItem(
medicine: medicines[index],
onToggle: () => toggleMedicine(index),
);
},
),
const SizedBox(height: 16),
Text(
'已服用: ${medicines.where((m) => m.taken).length}/${medicines.length}',
style: Theme.of(context).textTheme.bodyMedium,
),
],
),
);
}
}
class Medicine {
final String name;
final String dosage;
final String time;
final IconData icon;
bool taken;
Medicine({
required this.name,
required this.dosage,
required this.time,
required this.icon,
required this.taken,
});
}
实现要点
- 使用
StatefulWidget管理药物列表状态 - 定义
Medicine数据模型,包含药物名称、剂量、服用时间、图标和服用状态 - 实现
toggleMedicine方法处理药物服用状态的切换 - 使用
ListView.builder动态构建药物列表 - 实时计算并展示服用情况统计
单个药物项组件
medicine_item.dart 负责展示单个药物的详细信息,并处理点击交互效果。
import 'package:flutter/material.dart';
import 'medicine_reminder.dart';
class MedicineItem extends StatelessWidget {
final Medicine medicine;
final VoidCallback onToggle;
const MedicineItem({
super.key,
required this.medicine,
required this.onToggle,
});
Widget build(BuildContext context) {
return GestureDetector(
onTap: onToggle,
child: AnimatedContainer(
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut,
margin: const EdgeInsets.only(bottom: 12),
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: medicine.taken
? Theme.of(context).colorScheme.primary.withOpacity(0.1)
: Colors.grey[100],
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: medicine.taken
? Theme.of(context).colorScheme.primary
: Colors.grey[300]!,
width: 2,
),
boxShadow: medicine.taken
? [
BoxShadow(
color: Theme.of(context).colorScheme.primary.withOpacity(0.2),
blurRadius: 8,
offset: const Offset(0, 2),
),
]
: [],
),
child: Row(
children: [
Transform.scale(
scale: medicine.taken ? 1.1 : 1.0,
child: AnimatedContainer(
duration: const Duration(milliseconds: 300),
width: 48,
height: 48,
decoration: BoxDecoration(
color: medicine.taken
? Theme.of(context).colorScheme.primary
: Colors.grey[200],
borderRadius: BorderRadius.circular(24),
),
child: Icon(
medicine.icon,
color: medicine.taken ? Colors.white : Colors.grey[600],
),
),
),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
medicine.name,
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
fontWeight: medicine.taken ? FontWeight.bold : null,
color: medicine.taken
? Theme.of(context).colorScheme.primary
: null,
),
),
const SizedBox(height: 4),
Text(
'${medicine.dosage} · ${medicine.time}',
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: medicine.taken
? Theme.of(context).colorScheme.primary.withOpacity(0.7)
: Colors.grey[600],
),
),
],
),
),
AnimatedContainer(
duration: const Duration(milliseconds: 300),
width: 24,
height: 24,
decoration: BoxDecoration(
shape: BoxShape.circle,
border: Border.all(
color: medicine.taken
? Theme.of(context).colorScheme.primary
: Colors.grey[400]!,
width: 2,
),
color: medicine.taken
? Theme.of(context).colorScheme.primary
: Colors.transparent,
),
child: medicine.taken
? const Icon(
Icons.check,
size: 16,
color: Colors.white,
)
: null,
),
],
),
),
);
}
}
实现要点
- 使用
GestureDetector处理点击事件 - 使用
AnimatedContainer实现平滑的状态切换动画 - 使用
Transform.scale实现药物图标在服用状态下的缩放效果 - 通过
BoxDecoration实现不同状态的视觉区分 - 使用
Icon组件展示药物图标和服用状态图标
组件使用方法
-
在首页集成药物提醒功能
body: const MedicineReminder(), -
自定义药物列表
在_MedicineReminderState中修改medicines列表:List<Medicine> medicines = [ Medicine( name: '阿莫西林', dosage: '500mg', time: '08:00', taken: false, icon: Icons.medication, ), // 添加更多药物... ]; -
调整动画效果
修改AnimatedContainer的duration和curve参数:AnimatedContainer( duration: const Duration(milliseconds: 300), curve: Curves.easeInOut, // ... );
本次开发中容易遇到的问题
1. 动画实现问题
问题描述:在实现药物服用状态切换时,尝试使用 BoxDecoration 的 transform 参数实现缩放效果,但遇到语法错误。
解决方案:BoxDecoration 不支持 transform 参数,应使用 Transform.scale 包装 AnimatedContainer 来实现缩放效果。
示例代码:
// 错误示例
BoxDecoration(
// ...
transform: Matrix4.identity()..scale(1.1), // 不支持
// ...
);
// 正确示例
Transform.scale(
scale: medicine.taken ? 1.1 : 1.0,
child: AnimatedContainer(
// ...
),
);
2. 状态管理问题
问题描述:修改药物服用状态后,UI 没有及时更新。
解决方案:确保在状态变化时调用 setState() 方法,通知 Flutter 框架重建 UI。
示例代码:
void toggleMedicine(int index) {
setState(() {
medicines[index].taken = !medicines[index].taken;
});
}
3. 布局滚动问题
问题描述:在 Column 中使用 ListView.builder 时,出现布局溢出或滚动冲突。
解决方案:为 ListView.builder 添加 shrinkWrap: true 和 physics: const NeverScrollableScrollPhysics() 参数,使其适应父容器高度并禁用内部滚动。
示例代码:
ListView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: medicines.length,
itemBuilder: (context, index) {
// ...
},
);
4. 鸿蒙平台适配问题
问题描述:在鸿蒙设备上运行时,Flutter 动画效果可能不如预期流畅。
解决方案:
- 减少动画复杂度,使用更简单的过渡效果
- 确保使用
AnimatedContainer等内置动画组件,避免自定义动画 - 测试不同动画时长,找到适合鸿蒙平台的最佳值
5. 组件依赖问题
问题描述:在创建组件时,出现循环依赖错误。
解决方案:
- 确保组件之间的依赖关系清晰,避免循环导入
- 可以考虑将数据模型单独提取到一个文件中,避免循环依赖
总结本次开发中用到的技术点
1. Flutter 核心技术
状态管理
- 使用
StatefulWidget和setState()管理应用状态 - 实现了基于列表的动态状态更新
布局组件
- 使用
Column、Row构建基础布局 - 使用
ListView.builder高效渲染药物列表 - 使用
Scaffold构建应用基本结构
动画效果
- 使用
AnimatedContainer实现平滑的状态过渡 - 使用
Transform.scale实现药物图标缩放动画 - 使用
Curves.easeInOut优化动画曲线
交互处理
- 使用
GestureDetector处理点击事件 - 实现了响应式的用户交互反馈
2. 鸿蒙平台适配
混合工程结构
- 保持 Flutter 代码结构不变
- 利用 ohos_flutter 插件提供的鸿蒙集成能力
- 遵循鸿蒙应用的资源管理规范
性能优化
- 针对鸿蒙平台优化动画性能
- 确保组件渲染效率
- 合理使用 Flutter 的布局缓存机制
3. 开发最佳实践
组件化开发
- 将功能拆分为
MedicineReminder和MedicineItem两个组件 - 实现了组件间的清晰职责划分
- 通过回调函数实现组件间通信
代码组织
- 按功能模块划分文件结构
- 遵循 Flutter 的代码风格规范
- 使用有意义的变量和方法命名
用户体验
- 提供即时的视觉反馈
- 使用一致的设计语言
- 确保交互操作的流畅性
可维护性
- 代码结构清晰,易于理解
- 组件化设计便于后续扩展
- 预留了药物数据持久化的扩展空间
通过本次实战,我们成功实现了一个功能完整、交互友好的药物提醒应用,并适配了鸿蒙平台。该应用不仅满足了基本的药物管理需求,还通过精心设计的动画效果提升了用户体验。同时,我们也积累了 Flutter 应用适配鸿蒙平台的宝贵经验,为后续的跨平台开发打下了坚实基础。
欢迎加入开源鸿蒙跨平台社区: https://openharmonycrossplatform.csdn.net
更多推荐
所有评论(0)