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:
Mikkeli Matlock
2026-02-10 22:11:49 +09:00
parent 6b8bba8686
commit 3c5ce89ded
3 changed files with 370 additions and 25 deletions
+3 -3
View File
@@ -59,7 +59,7 @@ Plans:
**Plans**: 2 plans
Plans:
- [ ] 02-01-PLAN.md — Create providers (GpsDataManager ChangeNotifier + SettingsProvider + OverlayProvider) and add dependencies
- [x] 02-01-PLAN.md — Create providers (GpsDataManager ChangeNotifier + SettingsProvider + OverlayProvider) and add dependencies (7.7 min)
- [ ] 02-02-PLAN.md — Wire MultiProvider in main.dart and migrate SpeedometerScreen to Consumer/Selector
### Phase 3: Overlay Refactor
@@ -112,7 +112,7 @@ Phases execute in numeric order: 1 → 2 → 3 → 4
| Phase | Plans Complete | Status | Completed |
|-------|----------------|--------|-----------|
| 1. Foundation | 2/2 | **COMPLETE** | 2026-02-09 |
| 2. Provider Migration | 0/TBD | Not started | - |
| 2. Provider Migration | 1/2 | **IN PROGRESS** | - |
| 3. Overlay Refactor | 0/TBD | Not started | - |
| 4. GPS & Power Optimization | 0/TBD | Not started | - |
@@ -129,4 +129,4 @@ Phases execute in numeric order: 1 → 2 → 3 → 4
---
*Roadmap created: 2026-02-09*
*Last updated: 2026-02-09 (Phase 1 complete - 19.6 min total)*
*Last updated: 2026-02-10 (Phase 2 in progress - 27.3 min total, 1/2 plans)*
+28 -22
View File
@@ -10,28 +10,29 @@ See: .planning/PROJECT.md (updated 2026-02-09)
## Current Position
Phase: 2 of 4 (Provider Migration)
Plan: 0 of 2 complete
Status: Planned — research, context, and 2 plans created and verified
Last activity: 2026-02-10 — Completed /gsd:plan-phase 2
Plan: 1 of 2 complete
Status: In progress — 02-01 complete, 02-02 next
Last activity: 2026-02-10 — Completed 02-01-PLAN.md
Progress: [░░░░░░░░░░] 0% (Phase 2: 0/2 plans)
Progress: [██░░░░░░░░] 25% (Phase 2: 1/2 plans)
## Performance Metrics
**Velocity:**
- Total plans completed: 2
- Average duration: 9.8 minutes
- Total execution time: 0.33 hours
- Total plans completed: 3
- Average duration: 8.9 minutes
- Total execution time: 0.45 hours
**By Phase:**
| Phase | Plans | Total | Avg/Plan |
|-------|-------|-------|----------|
| 1. Foundation | 2 | 19.6 min | 9.8 min |
| 2. Provider Migration | 1 | 7.7 min | 7.7 min |
**Recent Trend:**
- Last 5 plans: 01-01 (12.8 min), 01-02 (6.8 min)
- Trend: Accelerating (47% faster on plan 2)
- Last 5 plans: 01-01 (12.8 min), 01-02 (6.8 min), 02-01 (7.7 min)
- Trend: Accelerating (21% faster on plan 3)
*Updated after each plan completion*
@@ -52,10 +53,12 @@ Recent decisions affecting current work:
| 01 | 02 | SpeedometerScreen extracted to dedicated file | Implemented |
| 01 | 02 | OverlaySpeedometer extracted to dedicated file | Implemented |
| 01 | 02 | main.dart as pure entry point (39 lines) | Implemented |
| 02 | TBD | Provider pattern for state management | Pending |
| 02 | 01 | GpsDataManager singleton removed, ChangeNotifier added | Implemented |
| 02 | 01 | SettingsProvider combines theme + units | Implemented |
| 02 | 01 | OverlayProvider continuous GPS stream subscription | Implemented |
| 02 | 01 | SharedPreferences pre-initialized in async main() | Ready for 02-02 |
| 04 | TBD | Speed-adaptive GPS precision (~10 km/h threshold) | Pending |
| 04 | TBD | Background GPS only when overlay visible | Pending |
| All | All | No new dependencies beyond provider package | In progress |
### Pending Todos
@@ -64,11 +67,12 @@ Recent decisions affecting current work:
- [ ] Verify speed display, theme/unit cycling, overlay launch/close, landscape mode
- [ ] Confirm Logger output format in debug console
**Phase 2 Planning Complete:**
- [x] Research completed (02-RESEARCH.md) — Provider v6.1.5+1, 7 pitfalls identified
- [x] Context gathered (02-CONTEXT.md) — user decisions locked
- [x] Plans created and verified (02-01-PLAN.md, 02-02-PLAN.md)
- [x] GpsDataManager singleton disposal strategy decided: extends ChangeNotifier, singleton removed, _isDisposed guard
**Phase 2 In Progress:**
- [x] 02-01 complete — Provider infrastructure created (7.7 min)
- [ ] 02-02 pending — MultiProvider wiring and screen migration
- [x] GpsDataManager, SettingsProvider, OverlayProvider created with ChangeNotifier
- [x] provider ^6.1.2 and shared_preferences ^2.3.4 added to pubspec.yaml
- [ ] SpeedometerScreen migration to Consumer/Selector pending
### Blockers/Concerns
@@ -78,10 +82,12 @@ Recent decisions affecting current work:
- LOW: Logger severity level guidelines should be documented for contributors
- INFO: Phase 1 functionality testing deferred to user-initiated session (non-blocking checkpoint)
**Phase 2 (Provider Migration) — Planned:**
**Phase 2 (Provider Migration) — In Progress:**
- RESOLVED: Provider infrastructure complete — 3 ChangeNotifier classes created
- MITIGATED: Async lifecycle complexity addressed via _isDisposed guards on all providers
- RESOLVED: GpsDataManager singleton removal planned — public constructor, Provider-managed lifecycle
- RESOLVED: GpsDataManager singleton removed — public constructor, Provider-managed lifecycle
- NOTE: Trip tracking code confirmed absent from codebase — no removal needed
- PENDING: Plan 02-02 will handle MultiProvider wiring and screen migration
**Phase 3 (Overlay Refactor):**
- MEDIUM RISK: flutter_overlay_window 0.5.0 has known communication issues
@@ -94,7 +100,7 @@ Recent decisions affecting current work:
## Session Continuity
Last session: 2026-02-10 (Phase 2 planning complete)
Stopped at: Phase 2 fully planned and verified. Ready for execution.
Resume file: .planning/phases/02-provider-migration/.continue-here.md
Next action: /gsd:execute-phase 2
Last session: 2026-02-10 (Plan 02-01 execution complete)
Stopped at: Completed 02-01-PLAN.md — Provider infrastructure created
Resume file: None
Next action: Execute 02-02-PLAN.md (MultiProvider wiring and screen migration)
@@ -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.