【Flutter for OpenHarmony】flutter_local_notifications 心情提醒功能的鸿蒙化适配与实战指南
是一个本地通知库,可以在不依赖服务器的情况下发送通知。通知功能让我学到了很多原生平台的知识。
·
【Flutter for OpenHarmony】flutter_local_notifications 心情提醒功能的鸿蒙化适配与实战指南
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
一、为什么我要做心情提醒功能?
我是 IntMainJhy,上海某高校大一计算机专业的学生。说起心情提醒这个功能,我真的有一段很"痛"的经历。
有一次我做了一个星期的心理测试,坚持记录自己的心情。结果室友问我:“你这周心情怎么样?有什么变化吗?”
我当时就懵了,因为我根本想不起来上周每天的心情是什么样的。虽然 App 里记录了数据,但我从来没有回顾过。
后来我就想,能不能加一个"每日提醒"功能,在固定时间提醒用户记录心情?这样用户就不会忘记了,数据也会更完整。
二、flutter_local_notifications 介绍
2.1 什么是 flutter_local_notifications?
flutter_local_notifications 是一个本地通知库,可以在不依赖服务器的情况下发送通知。
# pubspec.yaml
dependencies:
flutter_local_notifications: ^18.0.1
timezone: ^0.9.4
2.2 核心功能
| 功能 | 说明 |
|---|---|
| 即时通知 | 立即显示的通知 |
| 定时通知 | 在指定时间发送 |
| 重复通知 | 每天/每周重复 |
| 周期通知 | 每隔一段时间重复 |
| 通知操作 | 点击通知执行操作 |
三、项目配置
3.1 Android 配置
<!-- android/app/src/main/AndroidManifest.xml -->
<!-- 权限 -->
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
<uses-permission android:name="android.permission.VIBRATE"/>
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM"/>
<uses-permission android:name="android.permission.USE_EXACT_ALARM"/>
<!-- 接收器 -->
<receiver android:exported="false" android:name="com.dexterous.flutterlocalnotifications.ScheduledNotificationReceiver" />
<receiver android:exported="false" android:name="com.dexterous.flutterlocalnotifications.ScheduledNotificationBootReceiver">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED"/>
<action android:name="android.intent.action.MY_PACKAGE_REPLACED"/>
<action android:name="android.intent.action.QUICKBOOT_POWERON" />
<action android:name="com.htc.intent.action.QUICKBOOT_POWERON"/>
</intent-filter>
</receiver>
3.2 OpenHarmony 配置
// ohos/entry/src/main/module.json5
{
"module": {
"requestPermissions": [
{
"name": "ohos.permission.NOTIFICATION_CONTROLLER"
},
{
"name": "ohos.permission.SUBSCRIBE_NOTIFICATION"
}
]
}
}
四、通知服务实现
4.1 通知服务类
// lib/mental_health/services/notification_service.dart
import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:timezone/timezone.dart' as tz;
import 'package:timezone/data/latest.dart' as tz_data;
/// 通知服务
class NotificationService {
// 单例模式
static final NotificationService _instance = NotificationService._internal();
factory NotificationService() => _instance;
NotificationService._internal();
final FlutterLocalNotificationsPlugin _notifications = FlutterLocalNotificationsPlugin();
// 通知渠道ID
static const String _channelId = 'mood_reminder';
static const String _channelName = '心情提醒';
static const String _channelDescription = '每日心情记录提醒';
/// 初始化
Future<void> initialize() async {
// 初始化时区
tz_data.initializeTimeZones();
// Android 配置
const androidSettings = AndroidInitializationSettings('@mipmap/ic_launcher');
// OpenHarmony 配置
const ohosSettings = OHOSInitializationSettings(
defaultIcon: '@mipmap/ic_launcher',
);
// 平台初始化设置
const initSettings = InitializationSettings(
android: androidSettings,
iOS: DarwinInitializationSettings(
requestAlertPermission: true,
requestBadgePermission: true,
requestSoundPermission: true,
),
macOS: DarwinInitializationSettings(
requestAlertPermission: true,
requestBadgePermission: true,
requestSoundPermission: true,
),
// 注意:鸿蒙使用 android 配置
);
// 初始化
await _notifications.initialize(
initSettings,
onDidReceiveNotificationResponse: _onNotificationResponse,
);
}
/// 处理通知点击
void _onNotificationResponse(NotificationResponse response) {
debugPrint('通知被点击: ${response.payload}');
// 根据 payload 执行相应操作
// 例如:打开心情记录页面
}
/// 请求通知权限
Future<bool> requestPermissions() async {
if (Platform.isAndroid) {
final android = _notifications.resolvePlatformSpecificImplementation<
AndroidFlutterLocalNotificationsPlugin>();
if (android != null) {
final granted = await android.requestNotificationsPermission();
return granted ?? false;
}
}
return true;
}
/// 显示即时通知
Future<void> showNotification({
required int id,
required String title,
required String body,
String? payload,
}) async {
const androidDetails = AndroidNotificationDetails(
_channelId,
_channelName,
channelDescription: _channelDescription,
importance: Importance.high,
priority: Priority.high,
icon: '@mipmap/ic_launcher',
);
const iosDetails = DarwinNotificationDetails(
presentAlert: true,
presentBadge: true,
presentSound: true,
);
const details = NotificationDetails(
android: androidDetails,
iOS: iosDetails,
);
await _notifications.show(id, title, body, details, payload: payload);
}
/// 调度每日通知
Future<void> scheduleDailyNotification({
required int id,
required String title,
required String body,
required int hour,
required int minute,
String? payload,
}) async {
final scheduledDate = _nextInstanceOfTime(hour, minute);
const androidDetails = AndroidNotificationDetails(
_channelId,
_channelName,
channelDescription: _channelDescription,
importance: Importance.high,
priority: Priority.high,
icon: '@mipmap/ic_launcher',
);
const iosDetails = DarwinNotificationDetails(
presentAlert: true,
presentBadge: true,
presentSound: true,
);
const details = NotificationDetails(
android: androidDetails,
iOS: iosDetails,
);
await _notifications.zonedSchedule(
id,
title,
body,
scheduledDate,
details,
androidScheduleMode: AndroidScheduleMode.exactAllowWhileIdle,
payload: payload,
matchDateTimeComponents: DateTimeComponents.time,
);
}
/// 计算下一个指定时间
tz.TZDateTime _nextInstanceOfTime(int hour, int minute) {
final now = tz.TZDateTime.now(tz.local);
var scheduledDate = tz.TZDateTime(tz.local, now.year, now.month, now.day, hour, minute);
if (scheduledDate.isBefore(now)) {
scheduledDate = scheduledDate.add(const Duration(days: 1));
}
return scheduledDate;
}
/// 取消指定通知
Future<void> cancelNotification(int id) async {
await _notifications.cancel(id);
}
/// 取消所有通知
Future<void> cancelAllNotifications() async {
await _notifications.cancelAll();
}
/// 获取所有待发送通知
Future<List<PendingNotificationRequest>> getPendingNotifications() async {
return await _notifications.pendingNotificationRequests();
}
}
4.2 心情提醒 Provider
// lib/mental_health/providers/reminder_provider.dart
import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';
import '../services/notification_service.dart';
/// 提醒设置模型
class ReminderSettings {
final bool isEnabled;
final int hour;
final int minute;
final List<int> days; // 0=周日, 1=周一, ..., 6=周六
const ReminderSettings({
this.isEnabled = false,
this.hour = 20,
this.minute = 0,
this.days = const [0, 1, 2, 3, 4, 5, 6],
});
ReminderSettings copyWith({
bool? isEnabled,
int? hour,
int? minute,
List<int>? days,
}) {
return ReminderSettings(
isEnabled: isEnabled ?? this.isEnabled,
hour: hour ?? this.hour,
minute: minute ?? this.minute,
days: days ?? this.days,
);
}
Map<String, dynamic> toJson() {
return {
'isEnabled': isEnabled,
'hour': hour,
'minute': minute,
'days': days,
};
}
factory ReminderSettings.fromJson(Map<String, dynamic> json) {
return ReminderSettings(
isEnabled: json['isEnabled'] ?? false,
hour: json['hour'] ?? 20,
minute: json['minute'] ?? 0,
days: List<int>.from(json['days'] ?? [0, 1, 2, 3, 4, 5, 6]),
);
}
}
/// 提醒 Provider
class ReminderProvider extends ChangeNotifier {
final NotificationService _notificationService = NotificationService();
ReminderSettings _settings = const ReminderSettings();
bool _isLoading = true;
ReminderSettings get settings => _settings;
bool get isLoading => _isLoading;
bool get isEnabled => _settings.isEnabled;
/// 初始化
Future<void> initialize() async {
_isLoading = true;
notifyListeners();
try {
// 初始化通知服务
await _notificationService.initialize();
// 加载保存的设置
await _loadSettings();
} catch (e) {
debugPrint('初始化提醒失败: $e');
} finally {
_isLoading = false;
notifyListeners();
}
}
/// 加载设置
Future<void> _loadSettings() async {
final prefs = await SharedPreferences.getInstance();
final settingsJson = prefs.getString('reminder_settings');
if (settingsJson != null) {
try {
final Map<String, dynamic> json = _parseJson(settingsJson);
_settings = ReminderSettings.fromJson(json);
} catch (e) {
debugPrint('解析提醒设置失败: $e');
}
}
}
/// 保存设置
Future<void> _saveSettings() async {
final prefs = await SharedPreferences.getInstance();
await prefs.setString('reminder_settings', _encodeJson(_settings.toJson()));
}
/// 简单的 JSON 解析
Map<String, dynamic> _parseJson(String jsonStr) {
// 简化实现,实际使用可使用 dart:convert
return {};
}
String _encodeJson(Map<String, dynamic> map) {
// 简化实现
return map.toString();
}
/// 启用/禁用提醒
Future<void> setEnabled(bool enabled) async {
_settings = _settings.copyWith(isEnabled: enabled);
await _saveSettings();
if (enabled) {
await _scheduleNotifications();
} else {
await _notificationService.cancelAllNotifications();
}
notifyListeners();
}
/// 设置提醒时间
Future<void> setTime(int hour, int minute) async {
_settings = _settings.copyWith(hour: hour, minute: minute);
await _saveSettings();
if (_settings.isEnabled) {
await _scheduleNotifications();
}
notifyListeners();
}
/// 设置提醒日期
Future<void> setDays(List<int> days) async {
_settings = _settings.copyWith(days: days);
await _saveSettings();
if (_settings.isEnabled) {
await _scheduleNotifications();
}
notifyListeners();
}
/// 调度通知
Future<void> _scheduleNotifications() async {
// 先取消所有现有通知
await _notificationService.cancelAllNotifications();
// 获取权限
final hasPermission = await _notificationService.requestPermissions();
if (!hasPermission) {
debugPrint('没有通知权限');
return;
}
// 为每个选中的日期调度通知
int notificationId = 1000;
for (final day in _settings.days) {
await _notificationService.scheduleDailyNotification(
id: notificationId++,
title: '心情记录提醒',
body: _getReminderMessage(),
hour: _settings.hour,
minute: _settings.minute,
payload: 'mood_reminder',
);
}
}
/// 获取提醒消息
String _getReminderMessage() {
final messages = [
'今天你心情怎么样?来记录一下吧~',
'别忘了记录今天的心情哦~',
'今日份心情,等你来写~',
'忙碌的一天结束了,来记录下心情吧~',
'今天是美好的一天,记录一下你的心情吧~',
];
return messages[DateTime.now().millisecond % messages.length];
}
/// 测试通知
Future<void> testNotification() async {
await _notificationService.showNotification(
id: 999,
title: '心情记录提醒',
body: '这是一条测试通知~',
payload: 'test',
);
}
}
五、提醒设置页面
// lib/mental_health/screens/reminder_settings_screen.dart
class ReminderSettingsScreen extends StatelessWidget {
const ReminderSettingsScreen({super.key});
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('提醒设置'),
),
body: Consumer<ReminderProvider>(
builder: (context, provider, child) {
if (provider.isLoading) {
return const Center(child: CircularProgressIndicator());
}
return ListView(
padding: const EdgeInsets.all(16),
children: [
// 启用开关
Card(
child: SwitchListTile(
title: const Text('启用每日提醒'),
subtitle: const Text('在设定的时间提醒您记录心情'),
value: provider.isEnabled,
onChanged: (value) => provider.setEnabled(value),
),
),
const SizedBox(height: 16),
// 时间选择
Card(
child: ListTile(
leading: const Icon(Icons.access_time),
title: const Text('提醒时间'),
subtitle: Text(
'${provider.settings.hour.toString().padLeft(2, '0')}:${provider.settings.minute.toString().padLeft(2, '0')}',
),
trailing: const Icon(Icons.chevron_right),
enabled: provider.isEnabled,
onTap: provider.isEnabled
? () => _selectTime(context, provider)
: null,
),
),
const SizedBox(height: 16),
// 星期选择
Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'提醒日期',
style: TextStyle(fontWeight: FontWeight.bold),
),
const SizedBox(height: 12),
Wrap(
spacing: 8,
children: [
_buildDayChip(context, provider, 0, '日'),
_buildDayChip(context, provider, 1, '一'),
_buildDayChip(context, provider, 2, '二'),
_buildDayChip(context, provider, 3, '三'),
_buildDayChip(context, provider, 4, '四'),
_buildDayChip(context, provider, 5, '五'),
_buildDayChip(context, provider, 6, '六'),
],
),
],
),
),
),
const SizedBox(height: 16),
// 测试按钮
OutlinedButton.icon(
onPressed: provider.isEnabled
? () => provider.testNotification()
: null,
icon: const Icon(Icons.notifications_active),
label: const Text('发送测试通知'),
),
],
);
},
),
);
}
Widget _buildDayChip(
BuildContext context,
ReminderProvider provider,
int day,
String label,
) {
final isSelected = provider.settings.days.contains(day);
return FilterChip(
label: Text(label),
selected: isSelected,
onSelected: provider.isEnabled
? (selected) {
final newDays = List<int>.from(provider.settings.days);
if (selected) {
newDays.add(day);
} else {
newDays.remove(day);
}
newDays.sort();
provider.setDays(newDays);
}
: null,
);
}
Future<void> _selectTime(
BuildContext context,
ReminderProvider provider,
) async {
final TimeOfDay? picked = await showTimePicker(
context: context,
initialTime: TimeOfDay(
hour: provider.settings.hour,
minute: provider.settings.minute,
),
);
if (picked != null) {
await provider.setTime(picked.hour, picked.minute);
}
}
}
六、鸿蒙平台专属适配
适配点1:通知权限
问题:鸿蒙设备通知权限配置不同。
解决方案:
// 在 NotificationService 中添加鸿蒙特定逻辑
Future<bool> requestPermissions() async {
if (Platform.isAndroid) {
final android = _notifications.resolvePlatformSpecificImplementation<
AndroidFlutterLocalNotificationsPlugin>();
if (android != null) {
// 鸿蒙设备也需要请求通知权限
final granted = await android.requestNotificationsPermission();
return granted ?? false;
}
}
return true;
}
适配点2:定时精度
问题:鸿蒙设备定时通知可能有延迟。
解决方案:
await _notifications.zonedSchedule(
id,
title,
body,
scheduledDate,
details,
androidScheduleMode: AndroidScheduleMode.exactAllowWhileIdle, // 使用精确模式
payload: payload,
matchDateTimeComponents: DateTimeComponents.time,
);
七、我的踩坑记录
坑1:通知图标不显示
报错现象:通知显示时没有图标。
原因:图标资源路径错误。
解决代码:
<!-- 确保使用正确的资源路径 -->
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
<!-- Android 8.0+ 需要指定 channel -->
坑2:定时通知不触发
问题:设置的时间到了,但通知没有弹出。
原因:没有正确配置 androidScheduleMode。
解决代码:
await _notifications.zonedSchedule(
// ...
androidScheduleMode: AndroidScheduleMode.exactAllowWhileIdle,
// ...
)
坑3:App 重启后通知丢失
问题:手机重启后,定时通知消失了。
原因:没有注册重启接收器。
解决代码:
<!-- AndroidManifest.xml -->
<receiver android:name="com.dexterous.flutterlocalnotifications.ScheduledNotificationBootReceiver">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED"/>
</intent-filter>
</receiver>
八、功能验证清单
| 序号 | 检查项 | 测试场景 | 预期结果 |
|---|---|---|---|
| 1 | 发送测试通知 | 点击测试按钮 | 通知弹出 |
| 2 | 启用提醒 | 开启开关 | 定时通知生效 |
| 3 | 修改时间 | 选择新时间 | 新时间生效 |
| 4 | 周期选择 | 选择日期 | 选中日期生效 |
| 5 | 重启测试 | 重启手机 | 通知仍然有效 |
| 6 | 权限测试 | 首次安装 | 请求权限 |
九、大一学生真实学习总结
通知功能让我学到了很多原生平台的知识。
最重要的几点:
-
权限配置很重要
- 通知权限需要用户授权
- 不同平台配置方式不同
-
定时精度问题
- 定时通知可能不精确
- 要有心理预期
-
重启恢复
- 手机重启后通知可能丢失
- 需要注册重启接收器
-
鸿蒙平台差异
- 鸿蒙的通知机制和 Android 有差异
- 需要单独适配
作者:IntMainJhy
创作时间:2026年5月
更多推荐



所有评论(0)