Implement StatsAsyncNotifier with initial fetch and time window re-fetch
epic-activity-statistics-dashboard-state-and-realtime-task-002 — Implement the Riverpod AsyncNotifier that exposes AsyncValue<StatsSnapshot>. The notifier must trigger an initial fetch on construction by calling the stats repository, watch the selected time window provider and re-fetch automatically when the window changes, and expose a public invalidate() method that downstream listeners (the cache invalidator) can call. Use ref.watch to bind the time window dependency so Riverpod handles teardown correctly.
Acceptance Criteria
Technical Requirements
Execution Context
Tier 1 - 540 tasks
Can start after Tier 0 completes
Implementation Notes
AsyncNotifier.build() is the correct lifecycle hook — do NOT use a constructor or initState-style pattern. Place the debounce logic using a Timer inside the notifier if rapid time window changes are expected; cancel the timer in ref.onDispose. The invalidate() method should call `ref.invalidateSelf()` — this is the Riverpod-idiomatic way to force a re-build. Avoid using state = AsyncLoading() manually before the re-fetch; ref.invalidateSelf() handles this automatically.
If the project uses riverpod_generator, annotate with @riverpod and let code generation produce the provider — this is preferred over manual provider declaration for consistency. Ensure selectedTimeWindowProvider is watched (ref.watch) not read (ref.read) so Riverpod tracks the dependency correctly.
Testing Requirements
Unit tests using flutter_test and riverpod's ProviderContainer for isolated testing. Use mockito to mock StatsRepository. Test scenarios: (1) On creation, state transitions from AsyncLoading to AsyncData with the returned snapshot. (2) Repository throws → state becomes AsyncError.
(3) After error, invalidate() is called → state becomes AsyncLoading then AsyncData. (4) selectedTimeWindowProvider changes → build() is re-run and state re-fetches. Use ProviderContainer with overrides to inject the mock repository. Verify teardown: after container.dispose(), no callbacks fire.
Target ≥ 90% branch coverage for the notifier class.
Supabase realtime channel subscriptions that are not properly disposed on screen close can accumulate in memory across navigation events, causing duplicate invalidation calls, ghost fetches, and eventual memory leaks on long sessions.
Mitigation & Contingency
Mitigation: Implement StatsCacheInvalidator as a Riverpod provider with an explicit ref.onDispose callback that cancels the realtime channel subscription. Write a widget test that navigates away and back multiple times and asserts that only one subscription is active at any given time.
Contingency: If subscription leaks are found in production, add a global subscription registry that enforces at-most-one subscription per channel key, and schedule a dispose sweep on app background events.
Debouncing rapid inserts may swallow the invalidation signal if the debounce window outlasts the Supabase realtime event delivery window, resulting in the dashboard showing stale totals after a bulk registration completes.
Mitigation & Contingency
Mitigation: Set the debounce window to 800ms (shorter than the typical Supabase realtime delivery latency of 1-2s for batched events) and ensure the leading-edge invalidation fires immediately while trailing duplicates are suppressed. Integration-test with a 20-record bulk insert.
Contingency: If debounce timing proves unreliable, replace debounce with a trailing-edge timer reset on each event and add a guaranteed invalidation 5 seconds after the last event regardless of subsequent events.