🚀运行效果展示


Flutter框架跨平台鸿蒙开发------每日饮水APP的开发流程
前言
在快节奏的现代生活中,很多人常常忘记定时喝水,长期缺水会对身体健康造成不良影响。为了解决这个问题,我们开发了一款基于Flutter框架的跨平台每日饮水提醒APP,支持Android、iOS、Web和鸿蒙系统。本文将详细介绍该APP的开发流程、核心功能实现和技术要点。
应用介绍
🔍 功能概述
每日饮水APP是一款帮助用户养成良好饮水习惯的应用,主要功能包括:
- ✅ 实时显示今日喝水进度
- ✅ 支持快速添加喝水记录(100ml、200ml、300ml、500ml)
- ✅ 自定义提醒间隔(30-120分钟)
- ✅ 可设置每日喝水目标(1000-3000ml)
- ✅ 支持设置提醒时间范围
- ✅ 记录每次喝水的时间和量
- ✅ 定时推送喝水提醒通知
🎨 界面设计
应用采用简洁现代的设计风格,主要包含以下界面:
- 主界面:显示今日喝水进度、快速添加按钮和今日记录列表
- 设置界面:提供提醒设置功能,包括启用开关、提醒间隔、每日目标和提醒时间范围
技术栈选择
| 技术/框架 | 版本 | 用途 |
|---|---|---|
| Flutter | 3.6.2 | 跨平台UI框架 |
| Dart | 3.0.0 | 开发语言 |
| flutter_local_notifications | 19.5.0 | 本地通知 |
| shared_preferences | 2.5.3 | 数据存储 |
| timezone | 0.10.1 | 时区处理 |
| json_annotation | 4.9.0 | JSON序列化 |
开发流程和架构设计
📊 开发流程图
项目初始化
需求分析
技术选型
架构设计
UI设计
核心功能实现
测试
部署
维护和迭代
🏗️ 架构设计
应用采用分层架构设计,主要包含以下层次:
- 模型层 :定义数据模型,如
WaterReminderSettings和WaterLog - 服务层 :处理业务逻辑,如
StorageService和WaterReminderService - UI层 :负责界面展示,如
WaterReminderScreen和WaterReminderSettingsScreen
本地通知
数据存储
时区处理
UI层
服务层
模型层
外部依赖
flutter_local_notifications
shared_preferences
timezone
核心功能实现及代码展示
1. 数据模型设计
dart
// water_reminder_model.dart
import 'package:json_annotation/json_annotation.dart';
part 'water_reminder_model.g.dart';
/// 喝水提醒设置模型
@JsonSerializable()
class WaterReminderSettings {
/// 是否启用提醒
final bool isEnabled;
/// 提醒间隔(分钟)
final int reminderInterval;
/// 每日喝水目标(毫升)
final int dailyGoal;
/// 开始提醒时间
final int startHour;
/// 结束提醒时间
final int endHour;
/// 构造函数
WaterReminderSettings({
this.isEnabled = true,
this.reminderInterval = 60,
this.dailyGoal = 2000,
this.startHour = 8,
this.endHour = 22,
});
/// 从JSON创建模型
factory WaterReminderSettings.fromJson(Map<String, dynamic> json) =>
_$WaterReminderSettingsFromJson(json);
/// 转换为JSON
Map<String, dynamic> toJson() => _$WaterReminderSettingsToJson(this);
}
/// 喝水记录模型
@JsonSerializable()
class WaterLog {
/// 记录ID
final String id;
/// 喝水量(毫升)
final int amount;
/// 记录时间
final DateTime time;
/// 构造函数
WaterLog({
required this.id,
required this.amount,
required this.time,
});
/// 从JSON创建模型
factory WaterLog.fromJson(Map<String, dynamic> json) =>
_$WaterLogFromJson(json);
/// 转换为JSON
Map<String, dynamic> toJson() => _$WaterLogToJson(this);
}
2. 存储服务实现
dart
// storage_service.dart
import 'dart:convert';
import 'package:flutter/foundation.dart';
import 'package:shared_preferences/shared_preferences.dart';
import '../models/water_reminder_model.dart';
/// 本地存储服务
class StorageService {
/// 单例实例
static final StorageService _instance = StorageService._internal();
/// SharedPreferences实例
SharedPreferences? _prefs;
/// 构造函数
factory StorageService() => _instance;
/// 内部构造函数
StorageService._internal();
/// 初始化存储服务
Future<void> init() async {
try {
_prefs = await SharedPreferences.getInstance();
debugPrint('SharedPreferences初始化成功');
} catch (e) {
debugPrint('SharedPreferences初始化失败: $e');
_prefs = null;
}
}
/// 保存喝水提醒设置
Future<void> saveWaterReminderSettings(WaterReminderSettings settings) async {
final json = jsonEncode(settings.toJson());
await _prefs?.setString('water_reminder_settings', json);
}
/// 获取喝水提醒设置
Future<WaterReminderSettings> getWaterReminderSettings() async {
try {
if (_prefs == null) {
return WaterReminderSettings();
}
final json = _prefs!.getString('water_reminder_settings');
if (json == null) {
return WaterReminderSettings();
}
return WaterReminderSettings.fromJson(jsonDecode(json) as Map<String, dynamic>);
} catch (e) {
debugPrint('获取喝水提醒设置时出错: $e');
return WaterReminderSettings();
}
}
/// 保存喝水记录
Future<void> saveWaterLog(WaterLog log) async {
final jsonList = _prefs?.getString('water_logs') ?? '[]';
final List<dynamic> logs = jsonDecode(jsonList);
logs.add(log.toJson());
await _prefs?.setString('water_logs', jsonEncode(logs));
}
/// 获取今日喝水记录
Future<List<WaterLog>> getTodayWaterLogs() async {
try {
if (_prefs == null) {
return [];
}
final jsonList = _prefs?.getString('water_logs') ?? '[]';
final List<dynamic> logs = jsonDecode(jsonList);
final today = DateTime.now();
final todayStart = DateTime(today.year, today.month, today.day);
final todayEnd = DateTime(today.year, today.month, today.day, 23, 59, 59);
return logs
.map((e) => WaterLog.fromJson(e as Map<String, dynamic>))
.where((log) => log.time.isAfter(todayStart) && log.time.isBefore(todayEnd))
.toList();
} catch (e) {
debugPrint('获取今日喝水记录时出错: $e');
return [];
}
}
}
3. 提醒服务实现
dart
// water_reminder_service.dart
import 'package:flutter/foundation.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:timezone/timezone.dart' as tz;
import '../models/water_reminder_model.dart';
import 'storage_service.dart';
/// 喝水提醒服务
class WaterReminderService {
/// 单例实例
static final WaterReminderService _instance = WaterReminderService._internal();
/// 本地通知插件
final FlutterLocalNotificationsPlugin _flutterLocalNotificationsPlugin =
FlutterLocalNotificationsPlugin();
/// 存储服务
final StorageService _storageService = StorageService();
/// 当前提醒设置
WaterReminderSettings? _currentSettings;
/// 构造函数
factory WaterReminderService() => _instance;
/// 内部构造函数
WaterReminderService._internal();
/// 初始化服务
Future<void> init() async {
try {
// 初始化本地通知
await _initLocalNotifications();
// 加载设置
_currentSettings = await _storageService.getWaterReminderSettings();
// 根据设置启动或停止提醒
if (_currentSettings?.isEnabled ?? false) {
await startReminders();
}
} catch (e, stackTrace) {
debugPrint('初始化喝水提醒服务失败: $e');
debugPrint('堆栈跟踪: $stackTrace');
}
}
/// 初始化本地通知
Future<void> _initLocalNotifications() async {
// Android配置
const AndroidInitializationSettings initializationSettingsAndroid =
AndroidInitializationSettings('@mipmap/ic_launcher');
// iOS配置
const DarwinInitializationSettings initializationSettingsIOS =
DarwinInitializationSettings(
requestAlertPermission: true,
requestBadgePermission: true,
requestSoundPermission: true,
);
// 初始化设置
const InitializationSettings initializationSettings = InitializationSettings(
android: initializationSettingsAndroid,
iOS: initializationSettingsIOS,
);
await _flutterLocalNotificationsPlugin.initialize(initializationSettings);
}
/// 启动提醒
Future<void> startReminders() async {
if (_currentSettings == null || !_currentSettings!.isEnabled) {
return;
}
// 取消之前的所有提醒
await cancelAllReminders();
// 获取当前时间
final now = DateTime.now();
// 计算下一次提醒时间
DateTime nextReminderTime = now.add(
Duration(minutes: _currentSettings!.reminderInterval),
);
// 确保提醒时间在设定的范围内
nextReminderTime = _ensureTimeInRange(nextReminderTime);
// 调度通知
await _scheduleNotification(nextReminderTime);
debugPrint('喝水提醒已启动,下一次提醒时间:$nextReminderTime');
}
/// 确保时间在设定的范围内
DateTime _ensureTimeInRange(DateTime time) {
if (_currentSettings == null) {
return time;
}
final startHour = _currentSettings!.startHour;
final endHour = _currentSettings!.endHour;
// 如果时间在范围内,直接返回
if (time.hour >= startHour && time.hour < endHour) {
return time;
}
// 如果时间早于开始时间,设置为今天的开始时间
if (time.hour < startHour) {
return DateTime(
time.year,
time.month,
time.day,
startHour,
0,
0,
);
}
// 如果时间晚于结束时间,设置为明天的开始时间
return DateTime(
time.year,
time.month,
time.day + 1,
startHour,
0,
0,
);
}
/// 调度通知
Future<void> _scheduleNotification(DateTime scheduledTime) async {
// 转换为时区时间
final tz.TZDateTime tzScheduledTime = tz.TZDateTime.from(
scheduledTime,
tz.local,
);
// 创建通知详细信息
const NotificationDetails notificationDetails = NotificationDetails(
android: AndroidNotificationDetails(
'water_reminder_channel',
'喝水提醒',
channelDescription: '定时提醒您喝水',
importance: Importance.high,
priority: Priority.high,
),
iOS: DarwinNotificationDetails(),
);
// 调度通知
await _flutterLocalNotificationsPlugin.zonedSchedule(
0,
'喝水时间到!',
'记得喝一杯水哦,保持身体健康!',
tzScheduledTime,
notificationDetails,
androidScheduleMode: AndroidScheduleMode.exactAllowWhileIdle,
matchDateTimeComponents: DateTimeComponents.time,
);
}
/// 停止提醒
Future<void> stopReminders() async {
await cancelAllReminders();
debugPrint('喝水提醒已停止');
}
/// 取消所有提醒
Future<void> cancelAllReminders() async {
await _flutterLocalNotificationsPlugin.cancelAll();
}
/// 更新提醒设置
Future<void> updateSettings(WaterReminderSettings newSettings) async {
// 保存新设置
await _storageService.saveWaterReminderSettings(newSettings);
_currentSettings = newSettings;
// 根据新设置启动或停止提醒
if (newSettings.isEnabled) {
await startReminders();
} else {
await stopReminders();
}
}
}
4. 主界面实现
dart
// water_reminder_screen.dart
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import '../models/water_reminder_model.dart';
import '../services/storage_service.dart';
import 'water_reminder_settings_screen.dart';
/// 喝水提醒主屏幕
class WaterReminderScreen extends StatefulWidget {
/// 构造函数
const WaterReminderScreen({super.key});
@override
State<WaterReminderScreen> createState() => _WaterReminderScreenState();
}
class _WaterReminderScreenState extends State<WaterReminderScreen> {
/// 存储服务
final StorageService _storageService = StorageService();
/// 今日喝水记录
List<WaterLog> _todayLogs = [];
/// 提醒设置
WaterReminderSettings? _settings;
/// 今日喝水总量
int _todayTotal = 0;
/// 加载数据
Future<void> _loadData() async {
// 加载设置
final settings = await _storageService.getWaterReminderSettings();
// 加载今日记录
final logs = await _storageService.getTodayWaterLogs();
// 计算今日总量
final total = logs.fold(0, (sum, log) => sum + log.amount);
setState(() {
_settings = settings;
_todayLogs = logs;
_todayTotal = total;
});
}
/// 添加喝水记录
Future<void> _addWaterLog(int amount) async {
final log = WaterLog(
id: DateTime.now().millisecondsSinceEpoch.toString(),
amount: amount,
time: DateTime.now(),
);
await _storageService.saveWaterLog(log);
await _loadData();
}
@override
void initState() {
super.initState();
_loadData();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('喝水提醒'),
actions: [
IconButton(
icon: const Icon(Icons.settings),
onPressed: () async {
// 跳转到设置页面
final result = await Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const WaterReminderSettingsScreen(),
),
);
// 如果设置有变化,重新加载数据
if (result == true) {
await _loadData();
}
},
),
],
),
body: SingleChildScrollView(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// 喝水进度卡片
Card(
elevation: 4,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
child: Padding(
padding: const EdgeInsets.all(24.0),
child: Column(
children: [
const Text(
'今日喝水进度',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 24),
// 进度环 - 使用自定义绘制
SizedBox(
width: 200,
height: 200,
child: CustomPaint(
painter: _ProgressRingPainter(
progress: (_todayTotal / (_settings?.dailyGoal ?? 2000)).clamp(0.0, 1.0),
),
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'$_todayTotal',
style: const TextStyle(
fontSize: 48,
fontWeight: FontWeight.bold,
),
),
Text(
'ml / ${_settings?.dailyGoal ?? 2000}ml',
style: const TextStyle(
fontSize: 16,
color: Colors.grey,
),
),
],
),
),
),
),
const SizedBox(height: 24),
// 进度百分比
Text(
'${((_todayTotal / (_settings?.dailyGoal ?? 2000)) * 100).toInt()}%',
style: const TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
),
),
],
),
),
),
const SizedBox(height: 24),
// 快速添加按钮
const Text(
'快速添加',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 16),
// 使用Wrap替代GridView,避免溢出问题
Wrap(
spacing: 16.0,
runSpacing: 16.0,
alignment: WrapAlignment.spaceEvenly,
children: [
_buildQuickAddButton(100),
_buildQuickAddButton(200),
_buildQuickAddButton(300),
_buildQuickAddButton(500),
],
),
const SizedBox(height: 24),
// 今日记录
const Text(
'今日记录',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 16),
_todayLogs.isEmpty
? const Center(
child: Padding(
padding: EdgeInsets.all(24.0),
child: Text('今日还没有喝水记录哦'),
),
)
: ListView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: _todayLogs.length,
itemBuilder: (context, index) {
final log = _todayLogs[index];
return Card(
elevation: 2,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'${log.amount} ml',
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
Text(
DateFormat('HH:mm').format(log.time),
style: const TextStyle(
color: Colors.grey,
),
),
],
),
const Icon(
Icons.local_drink,
color: Colors.blue,
size: 32,
),
],
),
),
);
},
),
],
),
),
);
}
/// 构建快速添加按钮
Widget _buildQuickAddButton(int amount) {
return GestureDetector(
onTap: () => _addWaterLog(amount),
child: Card(
elevation: 2,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(
Icons.add,
color: Colors.blue,
size: 32,
),
const SizedBox(height: 8),
Text(
'$amount ml',
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
],
),
),
),
);
}
}
/// 自定义进度环绘制器
class _ProgressRingPainter extends CustomPainter {
/// 进度值(0-1)
final double progress;
/// 背景色
final Color backgroundColor;
/// 进度色
final Color progressColor;
/// 线宽
final double strokeWidth;
/// 构造函数
_ProgressRingPainter({
required this.progress,
this.backgroundColor = const Color(0xFFE0E0E0),
this.progressColor = Colors.blue,
this.strokeWidth = 15.0,
});
@override
void paint(Canvas canvas, Size size) {
// 计算中心点
final center = Offset(size.width / 2, size.height / 2);
// 计算半径
final radius = (size.width - strokeWidth) / 2;
// 创建背景画笔
final backgroundPaint = Paint()
..color = backgroundColor
..strokeWidth = strokeWidth
..style = PaintingStyle.stroke
..strokeCap = StrokeCap.round;
// 创建进度画笔
final progressPaint = Paint()
..color = progressColor
..strokeWidth = strokeWidth
..style = PaintingStyle.stroke
..strokeCap = StrokeCap.round;
// 绘制背景圆环
canvas.drawCircle(center, radius, backgroundPaint);
// 绘制进度圆环
final arcAngle = progress * 2 * 3.14159265359;
canvas.drawArc(
Rect.fromCircle(center: center, radius: radius),
-0.5 * 3.14159265359, // 起始角度(-90度)
arcAngle, // 扫描角度
false, // 是否使用中心点
progressPaint,
);
}
@override
bool shouldRepaint(covariant _ProgressRingPainter oldDelegate) {
return oldDelegate.progress != progress ||
oldDelegate.backgroundColor != backgroundColor ||
oldDelegate.progressColor != progressColor ||
oldDelegate.strokeWidth != strokeWidth;
}
}
5. 鸿蒙系统适配
为了支持鸿蒙系统,我们需要进行以下适配工作:
- 添加鸿蒙平台支持 :在
pubspec.yaml中添加鸿蒙平台配置 - 处理平台特定API:使用条件编译处理不同平台的API差异
- 测试鸿蒙设备:在鸿蒙设备或模拟器上进行测试
yaml
# pubspec.yaml
flutter:
uses-material-design: true
assets:
- assets/images/
# 添加鸿蒙平台支持
plugin:
platforms:
ohos:
package: com.example.flutter_text
pluginClass: FlutterTextPlugin
测试和部署
🧪 测试流程
- 单元测试:对核心功能进行单元测试
- 集成测试:测试不同组件之间的交互
- UI测试:测试界面布局和交互
- 跨平台测试:在不同平台上进行测试
🚀 部署流程
-
Web部署:
bashflutter build web -
Android部署:
bashflutter build apk -
iOS部署:
bashflutter build ios -
鸿蒙部署:
bashflutter build ohos
遇到的问题和解决方案
1. 进度环显示异常
问题 :在某些设备上,使用两个CircularProgressIndicator嵌套的方式显示进度环时,会出现异常显示。
解决方案 :使用自定义绘制的_ProgressRingPainter类来绘制进度环,确保进度环能够正确显示。
2. 快速添加按钮溢出
问题 :在小屏幕设备上,使用GridView.count时,快速添加按钮会溢出屏幕。
解决方案 :将GridView.count替换为Wrap组件,使按钮能够根据屏幕宽度自动换行。
3. 鸿蒙系统数据库初始化失败
问题 :在鸿蒙系统上,调用getApplicationDocumentsDirectory方法时会失败。
解决方案 :由于喝水提醒功能只使用SharedPreferences,不依赖数据库,因此跳过了数据库初始化步骤。
4. 时区初始化错误
问题 :在某些平台上,调用tz.initializeTimeZones()方法时会出现方法未找到的错误。
解决方案 :移除了tz.initializeTimeZones()方法调用,该方法在最新版本的timezone库中已不再需要。
总结和展望
📋 项目总结
通过本项目,我们成功开发了一款基于Flutter框架的跨平台每日饮水提醒APP,支持Android、iOS、Web和鸿蒙系统。应用具有以下特点:
- 跨平台兼容:使用Flutter框架实现一次开发,多平台运行
- 功能完整:包含喝水记录、提醒设置、进度展示等核心功能
- UI美观:采用现代化的设计风格,界面简洁易用
- 性能优良:优化了应用启动和运行性能
- 稳定性高:添加了异常处理机制,提高了应用的稳定性
🔮 未来展望
- 添加数据分析功能:分析用户的喝水习惯,提供个性化建议
- 支持多用户:添加用户登录功能,支持多设备同步
- 添加社交功能:支持用户之间的互动和分享
- 优化通知机制:提供更灵活的通知设置
- 支持更多语言:添加多语言支持
结论
Flutter框架为跨平台开发提供了强大的支持,使我们能够高效地开发出兼容多种平台的应用。通过本项目的实践,我们深入了解了Flutter框架的特性和鸿蒙系统的适配要点,积累了宝贵的跨平台开发经验。
每日饮水APP的开发不仅解决了用户忘记喝水的问题,也展示了Flutter框架在跨平台开发中的优势。我们相信,随着Flutter框架和鸿蒙系统的不断发展,跨平台开发将变得更加高效和便捷。
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net