Files
gps-speedo/.planning/phases/02-provider-migration/02-01-SUMMARY.md
T
Mikkeli Matlock 3c5ce89ded docs(02-01): complete provider infrastructure plan
Tasks completed: 3/3
- Task 1: Add provider dependencies and refactor GpsDataManager to ChangeNotifier
- Task 2: Create SettingsProvider for theme and unit state
- Task 3: Create OverlayProvider for overlay lifecycle and messaging

SUMMARY: .planning/phases/02-provider-migration/02-01-SUMMARY.md
2026-02-10 22:11:49 +09:00

15 KiB

phase, plan, subsystem, tags, requires, provides, affects, tech-stack, key-files, decisions, metrics
phase plan subsystem tags requires provides affects tech-stack key-files decisions metrics
02-provider-migration 01 state-management
provider
changenotifier
dependency-injection
settings-persistence
overlay-management
phase outputs
01
Logger
ProcessedGpsData
OverlayMessage
config constants
modular file structure
infrastructure
GpsDataManager as ChangeNotifier (reactive GPS data)
SettingsProvider (theme + unit persistence)
OverlayProvider (centralized overlay lifecycle)
provider ^6.1.2 and shared_preferences ^2.3.4 packages
phase plan why
02 02 Plan 02-02 will wire these providers via MultiProvider and migrate SpeedometerScreen
added patterns
provider
^6.1.2
shared_preferences
^2.3.4
ChangeNotifier for reactive state
_isDisposed guards for async safety
Constructor injection for SharedPreferences
Stream + ChangeNotifier dual access pattern
created modified
lib/providers/settings_provider.dart
lib/providers/overlay_provider.dart
lib/services/gps_data_manager.dart
pubspec.yaml
decision rationale impact alternatives confidence
GpsDataManager singleton removed, replaced with public constructor Provider manages lifecycle via dependency injection All GpsDataManager.instance calls must migrate to context.read/watch Keep singleton, wrap with separate provider class HIGH
decision rationale impact alternatives confidence
SettingsProvider combines theme + units in single provider Settings always change together in user flow, no performance benefit to splitting Single provider subscription for all settings UI Separate ThemeProvider and UnitProvider MEDIUM
decision rationale impact alternatives confidence
OverlayProvider listens to GPS stream continuously, skips sends when inactive Simpler than subscribe/unsubscribe on activate/deactivate, avoids subscription lifecycle bugs GPS stream always has one listener even when overlay closed Subscribe on activate, unsubscribe on deactivate MEDIUM
decision rationale impact alternatives confidence
SharedPreferences loaded eagerly in constructor, not async Pre-initialization in main() before runApp() eliminates flash of defaults main() must be async and call SharedPreferences.getInstance() before MultiProvider FutureProvider with async loading, show splash during load HIGH
duration completed
7.7 min 2026-02-10

Phase 02 Plan 01: Provider Infrastructure Summary

One-liner: Provider pattern foundation with ChangeNotifier-based GpsDataManager, SettingsProvider (theme+units), and OverlayProvider (lifecycle+messaging)

What Was Built

Created the three core Provider classes that will replace all setState() calls in SpeedometerScreen:

1. GpsDataManager Refactored (Task 1)

  • Removed singleton pattern — deleted static _instance and static get instance
  • Changed to public constructor — GpsDataManager._internal() → GpsDataManager()
  • Extended ChangeNotifier — added import 'package:flutter/foundation.dart'
  • Added reactive notifications — notifyListeners() called in _updateData() after stream emission
  • Async safety via _isDisposed guard — prevents "ChangeNotifier after dispose" errors
    • Guard added to: _updateData(), _onGpsError(), _handleStaleDataTimeout()
  • Proper disposal — @override dispose() sets _isDisposed = true, cleans up subscriptions/timers, calls super.dispose() last
  • Dual access pattern — Stream (for OverlayProvider) + ChangeNotifier (for UI widgets) coexist

2. SettingsProvider Created (Task 2)

  • Combines theme + speed unit — single provider for all user settings
  • Constructor injection — accepts pre-initialized SharedPreferences instance
  • Synchronous load — _loadSettings() reads from SharedPreferences in constructor
    • No notifyListeners() in constructor (runs before first build)
  • Persistence on change — cycleTheme() and cycleUnit() both:
    • Trigger haptic feedback via HapticFeedback.lightImpact()
    • Persist to SharedPreferences via _prefs.setInt()
    • Call notifyListeners() for reactive UI updates
  • Computed getter — currentTheme delegates to ColorThemes.getTheme(_currentThemeIndex)

3. OverlayProvider Created (Task 3)

  • Centralizes ALL overlay operations — show, close, toggle, status polling, IPC messaging
  • Dependency injection via updateDependencies() — called by ChangeNotifierProxyProvider2
    • Accepts GpsDataManager, SpeedUnit, int themeIndex
    • Subscribes to GPS stream on first call (persists for lifetime)
    • Pushes updated settings to overlay when active
  • Conditional messaging — if (_isOverlayActive && !_isDisposed) guards overlay sends
  • Stream subscription lifecycle — GPS stream subscribed once, never unsubscribed (just skip sends)
  • Overlay lifecycle management:
    • showOverlay() — calculates size, sends initial data with dimensions, launches overlay
    • closeOverlay() — closes overlay window via platform API
    • toggleOverlay() — convenience wrapper
    • _handleOverlayClose() — cleanup, optional bring-to-front
  • IPC handling — _startListeningToOverlayMessages() processes overlay→main messages
  • Status polling — 1-second timer checks FlutterOverlayWindow.isActive() for close detection
  • Public interface — pushCurrentData() for background heartbeat usage
  • Async safety — _isDisposed guards on all methods, disposes 4 subscriptions/timers

4. Dependencies Added

  • provider: ^6.1.2 — state management via ChangeNotifier/Consumer/Selector
  • shared_preferences: ^2.3.4 — persistent settings storage

Commits

Hash Type Message
f458b62 chore add provider dependencies and refactor GpsDataManager
1961651 feat create SettingsProvider for theme and unit state
6b8bba8 feat create OverlayProvider for overlay lifecycle and messaging

Deviations from Plan

None — plan executed exactly as written.

All three providers created with proper ChangeNotifier implementation, _isDisposed guards, and super.dispose() calls. Dependencies installed successfully.

Key Decisions Made

GpsDataManager Singleton Removal

Decision: Remove singleton pattern entirely, use Provider-managed lifecycle

Why: Provider's dependency injection handles instance lifecycle, disposal, and testing overrides automatically. Singleton creates hidden global state that bypasses Provider's benefits.

Implementation:

  • Deleted static GpsDataManager? _instance;
  • Deleted static GpsDataManager get instance => _instance ??= GpsDataManager._internal();
  • Changed GpsDataManager._internal() to public GpsDataManager()
  • Provider creates instance via ChangeNotifierProvider(create: (_) => GpsDataManager())

Impact: All existing GpsDataManager.instance calls in SpeedometerScreen will error — must migrate to context.read<GpsDataManager>() in Plan 02-02.

Verification: grep -E "static.*instance" lib/services/gps_data_manager.dart returns empty (singleton removed)

Settings Provider Consolidation

Decision: Combine theme and speed unit into single SettingsProvider

Why:

  1. Settings always change together in user flow (theme button, unit button side-by-side)
  2. No performance benefit to splitting — both are low-frequency updates
  3. Simpler dependency graph — one provider instead of two

Alternatives considered:

  • Separate ThemeProvider and UnitProvider — rejected due to unnecessary complexity
  • Single AppStateProvider for all state — rejected per context decision (granular providers)

Impact: UI widgets subscribe to one provider for both theme and unit data.

Overlay Stream Subscription Strategy

Decision: Subscribe to GPS stream once in updateDependencies(), skip sends when inactive via if-check

Why:

  1. Simpler lifecycle — no subscribe/unsubscribe dance
  2. Avoids "bad state: stream already listened to" errors
  3. Negligible performance impact (if-check vs subscription churn)

Alternatives considered:

  • Subscribe on activate, unsubscribe on deactivate — rejected due to async subscription timing bugs
  • Pause/resume stream subscription — rejected as StreamSubscription.pause() doesn't exist for broadcast streams

Implementation:

void updateDependencies({...}) {
  if (_gpsManager == null) {
    _gpsManager = gpsManager;
    _gpsSubscription = gpsManager.dataStream.listen((data) {
      if (_isOverlayActive && !_isDisposed) {  // Skip sends when inactive
        _pushDataToOverlay(data);
      }
    });
  }
}

SharedPreferences Pre-Initialization

Decision: Load SharedPreferences in main() before runApp(), inject into SettingsProvider constructor

Why:

  1. Eliminates "flash of defaults" on first app launch
  2. Makes SettingsProvider constructor synchronous (no async create callback)
  3. Matches Flutter best practice per provider research

Implementation pattern:

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  final prefs = await SharedPreferences.getInstance();
  runApp(
    MultiProvider(
      providers: [
        Provider.value(value: prefs),
        ChangeNotifierProvider(create: (ctx) => SettingsProvider(ctx.read<SharedPreferences>())),
      ],
      child: SpeedoApp(),
    ),
  );
}

Impact: Plan 02-02 must implement this async main() pattern.

Testing Notes

Verification Performed

  1. ✅ flutter pub get — dependencies resolve without conflicts
  2. ✅ flutter analyze — no errors in provider files (only expected SpeedometerScreen errors from removed singleton)
  3. ✅ GpsDataManager extends ChangeNotifier — verified via grep
  4. ✅ GpsDataManager calls notifyListeners() in _updateData() — verified via code inspection
  5. ✅ No singleton pattern in GpsDataManager — verified via grep (no static instance)
  6. ✅ SettingsProvider loads from SharedPreferences — constructor calls _loadSettings()
  7. ✅ SettingsProvider persists on change — cycleTheme/cycleUnit call _prefs.setInt()
  8. ✅ OverlayProvider has updateDependencies() — verified via grep
  9. ✅ OverlayProvider skips sends when inactive — if (_isOverlayActive && !_isDisposed) guards
  10. ✅ All providers have proper dispose() — @override, _isDisposed guard, super.dispose() last

Expected Errors (Will Fix in Plan 02-02)

error - The getter 'instance' isn't defined for the type 'GpsDataManager'
  lib\screens\speedometer_screen.dart:302:28
  lib\screens\speedometer_screen.dart:307:45
  lib\screens\speedometer_screen.dart:354:38
  lib\screens\speedometer_screen.dart:398:40

These are intentional — singleton removal breaks old code. Plan 02-02 will migrate all references to Provider-based access.

Manual Testing Required (Deferred to Plan 02-02)

  • App compiles after MultiProvider wiring
  • GPS data updates trigger UI rebuilds via Consumer/Selector
  • Theme/unit cycling persists across app restarts
  • Overlay launches and receives GPS data
  • No "ChangeNotifier after dispose" errors on hot reload
  • No "setState after dispose" errors on navigation

Integration Points

For Plan 02-02 (Screen Migration)

What's ready:

  • Three ChangeNotifier providers ready for MultiProvider wiring
  • GpsDataManager replaces direct GPS stream subscription
  • SettingsProvider replaces setState for theme/unit
  • OverlayProvider replaces inline overlay management code

What Plan 02-02 must do:

  1. Implement async main() with SharedPreferences.getInstance()
  2. Wire MultiProvider with dependency order:
    • Provider.value(prefs)
    • ChangeNotifierProvider(SettingsProvider)
    • ChangeNotifierProvider(GpsDataManager)
    • ChangeNotifierProxyProvider2(OverlayProvider) — depends on GPS + Settings
  3. Replace all GpsDataManager.instance calls with context.read/watch
  4. Replace setState for theme/unit with context.read<SettingsProvider>().cycleTheme/Unit()
  5. Replace inline overlay code with context.read<OverlayProvider>().showOverlay/closeOverlay()
  6. Wrap speed display with Selector<GpsDataManager, String> for granular rebuilds
  7. Wrap theme controls with Consumer<SettingsProvider> for reactive updates
  8. Call context.read<GpsDataManager>().initialize() in screen initState

Expected file changes in Plan 02-02:

  • lib/main.dart — async main(), MultiProvider setup
  • lib/screens/speedometer_screen.dart — remove setState, add Consumer/Selector, remove inline overlay code

Architecture Notes

ChangeNotifier Lifecycle Pattern

All three providers follow the same disposal pattern:

@override
void dispose() {
  _isDisposed = true;  // FIRST: Set guard flag
  // ... cancel all async operations (streams, timers, listeners)
  super.dispose();     // LAST: Call ChangeNotifier cleanup
}

Why _isDisposed guard: Async operations (GPS stream, overlay listeners, timers) may complete after dispose() called. Guard prevents "ChangeNotifier after dispose" assertion by short-circuiting methods that call notifyListeners().

Dual Access Pattern (GpsDataManager)

GpsDataManager provides two access patterns:

  1. Stream-based — dataStream getter for OverlayProvider's continuous subscription
  2. ChangeNotifier-based — currentData getter + notifyListeners() for UI widgets

Why both: OverlayProvider needs stream subscription to push data to overlay isolate. UI widgets prefer ChangeNotifier for reactive rebuilds. Both patterns share the same _currentData state and update simultaneously in _updateData().

Provider Dependency Graph

SharedPreferences (pre-initialized)
  └─> SettingsProvider (theme + unit)

GpsDataManager (independent, no deps)

OverlayProvider
  ├─> GpsDataManager (GPS data stream)
  └─> SettingsProvider (theme + unit for overlay)

Plan 02-02 will wire this via ChangeNotifierProxyProvider2 for OverlayProvider.

Next Phase Readiness

Blockers: NONE

All deliverables complete. Plan 02-02 can proceed immediately.

Concerns: LOW

Async main() timing: SharedPreferences.getInstance() adds ~50-100ms startup delay. Acceptable per research (eliminates flash of defaults).

Overlay stream persistence: GPS stream stays subscribed even when overlay closed. Negligible performance impact (if-check is O(1), subscription churn avoided).

Preparation for Plan 02-02

  • Provider classes created and verified
  • Dependencies installed and resolving
  • No circular dependencies between providers
  • All providers have proper dispose() implementations
  • _isDisposed guards prevent async disposal errors

Ready for execution: Plan 02-02 can wire providers and migrate screen without blockers.


Plan 02-01 complete. Three ChangeNotifier providers created with proper lifecycle management, async safety guards, and reactive notifications. Dependencies installed. Ready for MultiProvider wiring in Plan 02-02.