Integrate local notifications scheduler for reminders

This commit is contained in:
Rijad Zuzo
2026-02-15 23:48:29 +01:00
parent 960a891eba
commit 6e73273ede
10 changed files with 430 additions and 9 deletions
@@ -0,0 +1,182 @@
import 'package:flutter/foundation.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:relationship_saver/features/local/local_models.dart';
import 'package:relationship_saver/features/reminders/scheduling/reminder_scheduler.dart';
import 'package:timezone/timezone.dart' as tz;
const String _channelId = 'relationship_saver_reminders';
const String _channelName = 'Relationship Reminders';
const String _channelDescription =
'Reminders for relationship plans and follow-ups';
/// Schedules reminders with local notifications where platform support exists.
class LocalNotificationsReminderScheduler implements ReminderScheduler {
LocalNotificationsReminderScheduler({
ReminderNotificationsDriver? driver,
DateTime Function()? now,
}) : _driver = driver ?? FlutterReminderNotificationsDriver(),
_now = now ?? DateTime.now;
final ReminderNotificationsDriver _driver;
final DateTime Function() _now;
@override
Future<void> clearAll() async {
if (!_supportsSchedulingPlatform()) {
return;
}
await _driver.ensureInitialized();
await _driver.cancelAll();
}
@override
Future<void> reconcile(List<ReminderRule> reminders) async {
if (!_supportsSchedulingPlatform()) {
return;
}
await _driver.ensureInitialized();
await _driver.cancelAll();
final DateTime now = _now();
final List<ReminderRule> enabled = reminders
.where((ReminderRule reminder) => reminder.enabled)
.toList(growable: false);
for (final ReminderRule reminder in enabled) {
final DateTimeComponents? repeating = _repeatComponents(reminder.cadence);
if (repeating == null && reminder.nextAt.isBefore(now)) {
continue;
}
await _driver.schedule(
id: _notificationId(reminder.id),
title: reminder.title,
body: _buildBody(reminder),
scheduledAt: reminder.nextAt,
repeatComponents: repeating,
payload: reminder.id,
);
}
}
DateTimeComponents? _repeatComponents(ReminderCadence cadence) {
return switch (cadence) {
ReminderCadence.daily => DateTimeComponents.time,
ReminderCadence.weekly => DateTimeComponents.dayOfWeekAndTime,
ReminderCadence.monthly => DateTimeComponents.dayOfMonthAndTime,
};
}
String _buildBody(ReminderRule reminder) {
final String frequency = switch (reminder.cadence) {
ReminderCadence.daily => 'daily',
ReminderCadence.weekly => 'weekly',
ReminderCadence.monthly => 'monthly',
};
return 'Your $frequency reminder is due now.';
}
bool _supportsSchedulingPlatform() {
if (kIsWeb) {
return false;
}
return defaultTargetPlatform != TargetPlatform.linux;
}
int _notificationId(String raw) {
int hash = 0;
for (final int unit in raw.codeUnits) {
hash = ((hash * 31) + unit) & 0x7fffffff;
}
return hash;
}
}
/// Driver wrapper around the notification plugin for easier testing.
abstract class ReminderNotificationsDriver {
Future<void> ensureInitialized();
Future<void> cancelAll();
Future<void> schedule({
required int id,
required String title,
required String body,
required DateTime scheduledAt,
required DateTimeComponents? repeatComponents,
required String payload,
});
}
class FlutterReminderNotificationsDriver
implements ReminderNotificationsDriver {
FlutterReminderNotificationsDriver({FlutterLocalNotificationsPlugin? plugin})
: _plugin = plugin ?? FlutterLocalNotificationsPlugin();
final FlutterLocalNotificationsPlugin _plugin;
Future<void>? _initializeFuture;
@override
Future<void> ensureInitialized() {
return _initializeFuture ??= _initialize();
}
@override
Future<void> cancelAll() async {
await _plugin.cancelAll();
}
@override
Future<void> schedule({
required int id,
required String title,
required String body,
required DateTime scheduledAt,
required DateTimeComponents? repeatComponents,
required String payload,
}) async {
await _plugin.zonedSchedule(
id: id,
title: title,
body: body,
scheduledDate: tz.TZDateTime.from(scheduledAt.toUtc(), tz.UTC),
notificationDetails: _notificationDetails,
androidScheduleMode: AndroidScheduleMode.inexactAllowWhileIdle,
matchDateTimeComponents: repeatComponents,
payload: payload,
);
}
Future<void> _initialize() async {
await _plugin.initialize(
settings: const InitializationSettings(
android: AndroidInitializationSettings('@mipmap/ic_launcher'),
iOS: DarwinInitializationSettings(),
macOS: DarwinInitializationSettings(),
linux: LinuxInitializationSettings(defaultActionName: 'Open reminders'),
windows: WindowsInitializationSettings(
appName: 'Relationship Saver',
appUserModelId: 'com.relationshipsaver.app',
guid: 'ee23c0af-5898-4d96-b599-cf2d7d0d37d7',
),
),
);
}
NotificationDetails get _notificationDetails {
return const NotificationDetails(
android: AndroidNotificationDetails(
_channelId,
_channelName,
channelDescription: _channelDescription,
importance: Importance.high,
priority: Priority.high,
),
iOS: DarwinNotificationDetails(),
macOS: DarwinNotificationDetails(),
linux: LinuxNotificationDetails(),
windows: WindowsNotificationDetails(),
);
}
}