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
This commit is contained in:
@@ -0,0 +1,339 @@
|
||||
---
|
||||
phase: 02-provider-migration
|
||||
plan: 01
|
||||
subsystem: state-management
|
||||
tags: [provider, changenotifier, dependency-injection, settings-persistence, overlay-management]
|
||||
|
||||
requires:
|
||||
- phase: 01
|
||||
outputs: [Logger, ProcessedGpsData, OverlayMessage, config constants, modular file structure]
|
||||
|
||||
provides:
|
||||
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
|
||||
|
||||
affects:
|
||||
- phase: 02
|
||||
plan: 02
|
||||
why: Plan 02-02 will wire these providers via MultiProvider and migrate SpeedometerScreen
|
||||
|
||||
tech-stack:
|
||||
added:
|
||||
- provider: ^6.1.2
|
||||
- shared_preferences: ^2.3.4
|
||||
patterns:
|
||||
- ChangeNotifier for reactive state
|
||||
- _isDisposed guards for async safety
|
||||
- Constructor injection for SharedPreferences
|
||||
- Stream + ChangeNotifier dual access pattern
|
||||
|
||||
key-files:
|
||||
created:
|
||||
- lib/providers/settings_provider.dart
|
||||
- lib/providers/overlay_provider.dart
|
||||
modified:
|
||||
- lib/services/gps_data_manager.dart
|
||||
- pubspec.yaml
|
||||
|
||||
decisions:
|
||||
- decision: GpsDataManager singleton removed, replaced with public constructor
|
||||
rationale: Provider manages lifecycle via dependency injection
|
||||
impact: All GpsDataManager.instance calls must migrate to context.read/watch
|
||||
alternatives: Keep singleton, wrap with separate provider class
|
||||
confidence: HIGH
|
||||
|
||||
- decision: SettingsProvider combines theme + units in single provider
|
||||
rationale: Settings always change together in user flow, no performance benefit to splitting
|
||||
impact: Single provider subscription for all settings UI
|
||||
alternatives: Separate ThemeProvider and UnitProvider
|
||||
confidence: MEDIUM
|
||||
|
||||
- decision: OverlayProvider listens to GPS stream continuously, skips sends when inactive
|
||||
rationale: Simpler than subscribe/unsubscribe on activate/deactivate, avoids subscription lifecycle bugs
|
||||
impact: GPS stream always has one listener even when overlay closed
|
||||
alternatives: Subscribe on activate, unsubscribe on deactivate
|
||||
confidence: MEDIUM
|
||||
|
||||
- decision: SharedPreferences loaded eagerly in constructor, not async
|
||||
rationale: Pre-initialization in main() before runApp() eliminates flash of defaults
|
||||
impact: main() must be async and call SharedPreferences.getInstance() before MultiProvider
|
||||
alternatives: FutureProvider with async loading, show splash during load
|
||||
confidence: HIGH
|
||||
|
||||
metrics:
|
||||
duration: 7.7 min
|
||||
completed: 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:**
|
||||
```dart
|
||||
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:**
|
||||
```dart
|
||||
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:
|
||||
|
||||
```dart
|
||||
@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
|
||||
- [x] Provider classes created and verified
|
||||
- [x] Dependencies installed and resolving
|
||||
- [x] No circular dependencies between providers
|
||||
- [x] All providers have proper dispose() implementations
|
||||
- [x] _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.
|
||||
Reference in New Issue
Block a user