【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 权限测试 首次安装 请求权限

九、大一学生真实学习总结

通知功能让我学到了很多原生平台的知识。

最重要的几点:

  1. 权限配置很重要

    • 通知权限需要用户授权
    • 不同平台配置方式不同
  2. 定时精度问题

    • 定时通知可能不精确
    • 要有心理预期
  3. 重启恢复

    • 手机重启后通知可能丢失
    • 需要注册重启接收器
  4. 鸿蒙平台差异

    • 鸿蒙的通知机制和 Android 有差异
    • 需要单独适配

作者:IntMainJhy
创作时间:2026年5月

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐