- Fix NotificationService API calls to use flutter_local_notifications v21 named parameters - Expose nextInstanceOf as @visibleForTesting public method for unit testing - Create NotificationSettingsNotifier with @Riverpod(keepAlive: true) - NotificationSettings data class with enabled bool + TimeOfDay fields - Persist notifications_enabled, notifications_hour, notifications_minute to SharedPreferences - Sync default state in build(), async _load() overrides on hydration - Update tests to use correct Riverpod 3 provider name (notificationSettingsProvider) - Add makeContainer() helper to await initial _load() before asserting mutations - All 84 tests pass (72 existing + 12 new notification tests)
80 lines
2.5 KiB
Dart
80 lines
2.5 KiB
Dart
import 'package:flutter/foundation.dart';
|
|
import 'package:flutter/material.dart' show TimeOfDay;
|
|
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
|
|
import 'package:timezone/timezone.dart' as tz;
|
|
|
|
class NotificationService {
|
|
static final NotificationService _instance = NotificationService._internal();
|
|
factory NotificationService() => _instance;
|
|
NotificationService._internal();
|
|
|
|
final _plugin = FlutterLocalNotificationsPlugin();
|
|
|
|
Future<void> initialize() async {
|
|
const android = AndroidInitializationSettings('@mipmap/ic_launcher');
|
|
const settings = InitializationSettings(android: android);
|
|
await _plugin.initialize(
|
|
settings: settings,
|
|
onDidReceiveNotificationResponse: _onTap,
|
|
);
|
|
}
|
|
|
|
Future<bool> requestPermission() async {
|
|
final android = _plugin.resolvePlatformSpecificImplementation<
|
|
AndroidFlutterLocalNotificationsPlugin>();
|
|
return await android?.requestNotificationsPermission() ?? false;
|
|
}
|
|
|
|
Future<void> scheduleDailyNotification({
|
|
required TimeOfDay time,
|
|
required String title,
|
|
required String body,
|
|
}) async {
|
|
await _plugin.cancelAll();
|
|
final scheduledDate = nextInstanceOf(time);
|
|
const details = NotificationDetails(
|
|
android: AndroidNotificationDetails(
|
|
'daily_summary',
|
|
'Tägliche Zusammenfassung',
|
|
channelDescription: 'Tägliche Aufgaben-Erinnerung',
|
|
importance: Importance.defaultImportance,
|
|
priority: Priority.defaultPriority,
|
|
),
|
|
);
|
|
await _plugin.zonedSchedule(
|
|
id: 0,
|
|
title: title,
|
|
body: body,
|
|
scheduledDate: scheduledDate,
|
|
notificationDetails: details,
|
|
androidScheduleMode: AndroidScheduleMode.inexactAllowWhileIdle,
|
|
matchDateTimeComponents: DateTimeComponents.time,
|
|
);
|
|
}
|
|
|
|
Future<void> cancelAll() => _plugin.cancelAll();
|
|
|
|
/// Computes the next occurrence of [time] as a [tz.TZDateTime].
|
|
/// Returns today if [time] is still in the future, tomorrow otherwise.
|
|
@visibleForTesting
|
|
tz.TZDateTime nextInstanceOf(TimeOfDay time) {
|
|
final now = tz.TZDateTime.now(tz.local);
|
|
var scheduled = tz.TZDateTime(
|
|
tz.local,
|
|
now.year,
|
|
now.month,
|
|
now.day,
|
|
time.hour,
|
|
time.minute,
|
|
);
|
|
if (scheduled.isBefore(now)) {
|
|
scheduled = scheduled.add(const Duration(days: 1));
|
|
}
|
|
return scheduled;
|
|
}
|
|
|
|
static void _onTap(NotificationResponse response) {
|
|
// Navigation to Home tab — wired in Plan 02 via global navigator key
|
|
}
|
|
}
|