Wire reminder scheduler interface into local repository

This commit is contained in:
Rijad Zuzo
2026-02-15 20:16:38 +01:00
parent f1442e89d8
commit 5228799abb
6 changed files with 141 additions and 5 deletions
+13
View File
@@ -41,6 +41,19 @@ Disable it explicitly:
flutter run --dart-define=ENABLE_BACKGROUND_SYNC=false
```
## Reminder Delivery Scaffold
Reminder scheduling is now wired through `ReminderScheduler` and reconciled from
`LocalRepository` on startup/state changes.
Current default implementation is no-op:
- `NoopReminderScheduler` in
`lib/features/reminders/scheduling/reminder_scheduler.dart`
This keeps behavior stable across all platforms until real local notification
integration is added.
## Local Persistence Backend
Default local store:
+28 -3
View File
@@ -2,6 +2,31 @@
Updated: 2026-02-15
## Latest Milestone (2026-02-15): Reminder Delivery Interface Wiring
Implemented the notification scheduling boundary and connected it to local data
state transitions:
- Added reminder scheduler abstraction:
- `lib/features/reminders/scheduling/reminder_scheduler.dart`
- `ReminderScheduler` + `NoopReminderScheduler`
- Added provider wiring:
- `lib/features/reminders/scheduling/reminder_scheduler_provider.dart`
- Integrated schedule reconciliation with local repository:
- `lib/features/local/local_repository.dart`
- reconcile runs on repository boot and each persisted state update
- failures are swallowed to preserve offline-first behavior
- Added tests:
- `test/features/local/local_repository_test.dart` now verifies reminder
schedule reconciliation calls
- Updated setup docs:
- `docs/SETUP.md` now documents reminder scheduler scaffold
Validation for this milestone:
- `flutter analyze` -> pass
- `flutter test` -> pass
## Latest Milestone (2026-02-15): Sync Hardening + CI Baseline
Implemented the next production-readiness slice:
@@ -36,9 +61,9 @@ Validation for this milestone:
### Open Phases (Current)
1. Reminder delivery integration:
- add platform notification scheduler (local notifications) and wire reminder
CRUD/state changes to schedule/cancel/update jobs.
1. Reminder delivery runtime integration:
- replace no-op scheduler with platform local notifications implementation
for Android/iOS/macOS/Windows/Linux/Web-safe fallback.
2. Conflict resolution UX:
- current UX shows and dismisses rejections; still missing guided
resolve/retry/inspect flows per entity.
+15 -1
View File
@@ -7,6 +7,7 @@ import 'package:relationship_saver/features/local/storage/local_data_store.dart'
import 'package:relationship_saver/features/local/storage/local_data_store_hive.dart';
import 'package:relationship_saver/features/local/storage/local_data_store_provider.dart';
import 'package:relationship_saver/features/local/storage/local_data_store_shared_prefs.dart';
import 'package:relationship_saver/features/reminders/scheduling/reminder_scheduler_provider.dart';
import 'package:relationship_saver/features/sync/sync_queue_repository.dart';
import 'package:relationship_saver/integrations/backend/models/backend_models.dart';
import 'package:uuid/uuid.dart';
@@ -41,6 +42,7 @@ class LocalRepository extends AsyncNotifier<LocalDataState> {
if (record == null || record.rawState.isEmpty) {
final LocalDataState seeded = LocalDataState.seed();
await _persist(seeded);
await _reconcileReminderSchedule(seeded);
return seeded;
}
@@ -48,10 +50,13 @@ class LocalRepository extends AsyncNotifier<LocalDataState> {
try {
final Map<String, dynamic> json =
jsonDecode(migrated.rawState) as Map<String, dynamic>;
return LocalDataState.fromJson(json);
final LocalDataState loaded = LocalDataState.fromJson(json);
await _reconcileReminderSchedule(loaded);
return loaded;
} on FormatException {
final LocalDataState fallback = LocalDataState.seed();
await _persist(fallback);
await _reconcileReminderSchedule(fallback);
return fallback;
}
}
@@ -889,6 +894,7 @@ class LocalRepository extends AsyncNotifier<LocalDataState> {
Future<void> _setState(LocalDataState next) async {
state = AsyncData<LocalDataState>(next);
await _persist(next);
await _reconcileReminderSchedule(next);
}
LocalDataState _requireState() {
@@ -908,6 +914,14 @@ class LocalRepository extends AsyncNotifier<LocalDataState> {
);
}
Future<void> _reconcileReminderSchedule(LocalDataState state) async {
try {
await ref.read(reminderSchedulerProvider).reconcile(state.reminders);
} catch (_) {
// Scheduling failures must not block local-first data updates.
}
}
Future<LocalDataRecord> _migrateIfNeeded(LocalDataRecord record) async {
final int currentVersion = record.schemaVersion;
if (currentVersion == _schemaVersion) {
@@ -0,0 +1,21 @@
import 'package:relationship_saver/features/local/local_models.dart';
/// Schedules and reconciles reminder deliveries for current local state.
abstract class ReminderScheduler {
/// Reconciles platform schedules to match current enabled reminders.
Future<void> reconcile(List<ReminderRule> reminders);
/// Clears all platform-scheduled reminder jobs.
Future<void> clearAll();
}
/// Default scheduler used until platform notifications are integrated.
class NoopReminderScheduler implements ReminderScheduler {
const NoopReminderScheduler();
@override
Future<void> clearAll() async {}
@override
Future<void> reconcile(List<ReminderRule> reminders) async {}
}
@@ -0,0 +1,8 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:relationship_saver/features/reminders/scheduling/reminder_scheduler.dart';
/// Provides reminder delivery scheduler implementation.
final Provider<ReminderScheduler> reminderSchedulerProvider =
Provider<ReminderScheduler>((Ref ref) {
return const NoopReminderScheduler();
});
+56 -1
View File
@@ -4,6 +4,8 @@ import 'package:relationship_saver/features/local/local_models.dart';
import 'package:relationship_saver/features/local/local_repository.dart';
import 'package:relationship_saver/features/local/storage/local_data_store_in_memory.dart';
import 'package:relationship_saver/features/local/storage/local_data_store_provider.dart';
import 'package:relationship_saver/features/reminders/scheduling/reminder_scheduler.dart';
import 'package:relationship_saver/features/reminders/scheduling/reminder_scheduler_provider.dart';
import 'package:relationship_saver/features/sync/storage/sync_state_store_in_memory.dart';
import 'package:relationship_saver/features/sync/storage/sync_state_store_provider.dart';
import 'package:shared_preferences/shared_preferences.dart';
@@ -175,13 +177,66 @@ void main() {
);
expect(afterDelete.reminders.length, beforeCount);
});
test('reconciles reminder schedule after state changes', () async {
final RecordingReminderScheduler scheduler = RecordingReminderScheduler();
final ProviderContainer container = _createContainer(scheduler: scheduler);
addTearDown(container.dispose);
final LocalDataState initial = await container.read(
localRepositoryProvider.future,
);
final int beforeCount = initial.reminders.length;
await container
.read(localRepositoryProvider.notifier)
.addReminder(
title: 'Scheduled reminder',
cadence: ReminderCadence.daily,
nextAt: DateTime.now().add(const Duration(hours: 4)),
);
final LocalDataState afterAdd = await container.read(
localRepositoryProvider.future,
);
await container
.read(localRepositoryProvider.notifier)
.deleteReminder(afterAdd.reminders.first.id);
final LocalDataState afterDelete = await container.read(
localRepositoryProvider.future,
);
expect(scheduler.snapshots, isNotEmpty);
expect(scheduler.snapshots.first.length, beforeCount);
expect(
scheduler.snapshots.any(
(List<ReminderRule> r) => r.length == beforeCount + 1,
),
isTrue,
);
expect(scheduler.snapshots.last.length, afterDelete.reminders.length);
});
}
ProviderContainer _createContainer() {
ProviderContainer _createContainer({ReminderScheduler? scheduler}) {
return ProviderContainer(
overrides: [
localDataStoreProvider.overrideWithValue(InMemoryLocalDataStore()),
syncStateStoreProvider.overrideWithValue(InMemorySyncStateStore()),
if (scheduler != null)
reminderSchedulerProvider.overrideWithValue(scheduler),
],
);
}
class RecordingReminderScheduler implements ReminderScheduler {
final List<List<ReminderRule>> snapshots = <List<ReminderRule>>[];
@override
Future<void> clearAll() async {}
@override
Future<void> reconcile(List<ReminderRule> reminders) async {
snapshots.add(List<ReminderRule>.unmodifiable(reminders));
}
}