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
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 |
|
|
|
|
|
|
|
|
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 _instanceandstatic 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()
- Guard added to:
- Proper disposal —
@override dispose()sets_isDisposed = true, cleans up subscriptions/timers, callssuper.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
SharedPreferencesinstance - Synchronous load —
_loadSettings()reads from SharedPreferences in constructor- No
notifyListeners()in constructor (runs before first build)
- No
- Persistence on change —
cycleTheme()andcycleUnit()both:- Trigger haptic feedback via
HapticFeedback.lightImpact() - Persist to SharedPreferences via
_prefs.setInt() - Call
notifyListeners()for reactive UI updates
- Trigger haptic feedback via
- Computed getter —
currentThemedelegates toColorThemes.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
- Accepts
- 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 overlaycloseOverlay()— closes overlay window via platform APItoggleOverlay()— 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 —
_isDisposedguards 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 publicGpsDataManager() - 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:
- Settings always change together in user flow (theme button, unit button side-by-side)
- No performance benefit to splitting — both are low-frequency updates
- 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:
- Simpler lifecycle — no subscribe/unsubscribe dance
- Avoids "bad state: stream already listened to" errors
- 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:
- Eliminates "flash of defaults" on first app launch
- Makes SettingsProvider constructor synchronous (no async create callback)
- 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
- ✅
flutter pub get— dependencies resolve without conflicts - ✅
flutter analyze— no errors in provider files (only expected SpeedometerScreen errors from removed singleton) - ✅ GpsDataManager extends ChangeNotifier — verified via grep
- ✅ GpsDataManager calls notifyListeners() in _updateData() — verified via code inspection
- ✅ No singleton pattern in GpsDataManager — verified via grep (no static instance)
- ✅ SettingsProvider loads from SharedPreferences — constructor calls _loadSettings()
- ✅ SettingsProvider persists on change — cycleTheme/cycleUnit call _prefs.setInt()
- ✅ OverlayProvider has updateDependencies() — verified via grep
- ✅ OverlayProvider skips sends when inactive —
if (_isOverlayActive && !_isDisposed)guards - ✅ 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:
- Implement async main() with SharedPreferences.getInstance()
- Wire MultiProvider with dependency order:
- Provider.value(prefs)
- ChangeNotifierProvider(SettingsProvider)
- ChangeNotifierProvider(GpsDataManager)
- ChangeNotifierProxyProvider2(OverlayProvider) — depends on GPS + Settings
- Replace all
GpsDataManager.instancecalls withcontext.read/watch - Replace setState for theme/unit with
context.read<SettingsProvider>().cycleTheme/Unit() - Replace inline overlay code with
context.read<OverlayProvider>().showOverlay/closeOverlay() - Wrap speed display with
Selector<GpsDataManager, String>for granular rebuilds - Wrap theme controls with
Consumer<SettingsProvider>for reactive updates - 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:
- Stream-based —
dataStreamgetter for OverlayProvider's continuous subscription - ChangeNotifier-based —
currentDatagetter +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.