feat(04-01): NotificationSettingsNotifier with SharedPreferences persistence and full test suite

- 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)
This commit is contained in:
2026-03-16 14:57:04 +01:00
parent 0f6789becd
commit 4f72eac933
4 changed files with 151 additions and 31 deletions

View File

@@ -14,15 +14,14 @@ class NotificationService {
const android = AndroidInitializationSettings('@mipmap/ic_launcher');
const settings = InitializationSettings(android: android);
await _plugin.initialize(
settings,
settings: settings,
onDidReceiveNotificationResponse: _onTap,
);
}
Future<bool> requestPermission() async {
final android = _plugin
.resolvePlatformSpecificImplementation<
AndroidFlutterLocalNotificationsPlugin>();
final android = _plugin.resolvePlatformSpecificImplementation<
AndroidFlutterLocalNotificationsPlugin>();
return await android?.requestNotificationsPermission() ?? false;
}
@@ -32,7 +31,7 @@ class NotificationService {
required String body,
}) async {
await _plugin.cancelAll();
final scheduledDate = _nextInstanceOf(time);
final scheduledDate = nextInstanceOf(time);
const details = NotificationDetails(
android: AndroidNotificationDetails(
'daily_summary',
@@ -43,11 +42,11 @@ class NotificationService {
),
);
await _plugin.zonedSchedule(
0,
id: 0,
title: title,
body: body,
scheduledDate: scheduledDate,
details,
notificationDetails: details,
androidScheduleMode: AndroidScheduleMode.inexactAllowWhileIdle,
matchDateTimeComponents: DateTimeComponents.time,
);

View File

@@ -0,0 +1,52 @@
import 'package:flutter/material.dart' show TimeOfDay;
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:shared_preferences/shared_preferences.dart';
part 'notification_settings_notifier.g.dart';
class NotificationSettings {
final bool enabled;
final TimeOfDay time;
const NotificationSettings({required this.enabled, required this.time});
}
@Riverpod(keepAlive: true)
class NotificationSettingsNotifier extends _$NotificationSettingsNotifier {
static const _enabledKey = 'notifications_enabled';
static const _hourKey = 'notifications_hour';
static const _minuteKey = 'notifications_minute';
@override
NotificationSettings build() {
_load();
return const NotificationSettings(
enabled: false,
time: TimeOfDay(hour: 7, minute: 0),
);
}
Future<void> _load() async {
final prefs = await SharedPreferences.getInstance();
final enabled = prefs.getBool(_enabledKey) ?? false;
final hour = prefs.getInt(_hourKey) ?? 7;
final minute = prefs.getInt(_minuteKey) ?? 0;
state = NotificationSettings(
enabled: enabled,
time: TimeOfDay(hour: hour, minute: minute),
);
}
Future<void> setEnabled(bool enabled) async {
state = NotificationSettings(enabled: enabled, time: state.time);
final prefs = await SharedPreferences.getInstance();
await prefs.setBool(_enabledKey, enabled);
}
Future<void> setTime(TimeOfDay time) async {
state = NotificationSettings(enabled: state.enabled, time: time);
final prefs = await SharedPreferences.getInstance();
await prefs.setInt(_hourKey, time.hour);
await prefs.setInt(_minuteKey, time.minute);
}
}

View File

@@ -0,0 +1,65 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'notification_settings_notifier.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint, type=warning
@ProviderFor(NotificationSettingsNotifier)
final notificationSettingsProvider = NotificationSettingsNotifierProvider._();
final class NotificationSettingsNotifierProvider
extends
$NotifierProvider<NotificationSettingsNotifier, NotificationSettings> {
NotificationSettingsNotifierProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'notificationSettingsProvider',
isAutoDispose: false,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$notificationSettingsNotifierHash();
@$internal
@override
NotificationSettingsNotifier create() => NotificationSettingsNotifier();
/// {@macro riverpod.override_with_value}
Override overrideWithValue(NotificationSettings value) {
return $ProviderOverride(
origin: this,
providerOverride: $SyncValueProvider<NotificationSettings>(value),
);
}
}
String _$notificationSettingsNotifierHash() =>
r'0d04ca73c4724bb84ce8d92608cd238cb362254a';
abstract class _$NotificationSettingsNotifier
extends $Notifier<NotificationSettings> {
NotificationSettings build();
@$mustCallSuper
@override
void runBuild() {
final ref = this.ref as $Ref<NotificationSettings, NotificationSettings>;
final element =
ref.element
as $ClassProviderElement<
AnyNotifier<NotificationSettings, NotificationSettings>,
NotificationSettings,
Object?,
Object?
>;
element.handleCreate(ref, build);
}
}