630cf1c578
Phase 02: Provider Migration - 2 plan(s) in 2 wave(s) - Wave 1: Create providers (GpsDataManager + SettingsProvider + OverlayProvider) - Wave 2: Wire MultiProvider and migrate SpeedometerScreen - Ready for execution
540 lines
21 KiB
Markdown
540 lines
21 KiB
Markdown
---
|
|
phase: 02-provider-migration
|
|
plan: 01
|
|
type: execute
|
|
wave: 1
|
|
depends_on: []
|
|
files_modified:
|
|
- pubspec.yaml
|
|
- lib/services/gps_data_manager.dart
|
|
- lib/providers/settings_provider.dart
|
|
- lib/providers/overlay_provider.dart
|
|
autonomous: true
|
|
|
|
must_haves:
|
|
truths:
|
|
- "GpsDataManager extends ChangeNotifier and calls notifyListeners() on GPS data updates"
|
|
- "GpsDataManager no longer has a singleton accessor — no static _instance or get instance"
|
|
- "SettingsProvider loads theme index and speed unit from SharedPreferences synchronously in constructor"
|
|
- "SettingsProvider persists theme and unit changes to SharedPreferences on every cycle"
|
|
- "OverlayProvider has activate/deactivate methods and skips overlay sends when inactive"
|
|
- "provider and shared_preferences packages are added to pubspec.yaml dependencies"
|
|
artifacts:
|
|
- path: "lib/services/gps_data_manager.dart"
|
|
provides: "GPS data ChangeNotifier with _isDisposed guard"
|
|
contains: "extends ChangeNotifier"
|
|
- path: "lib/providers/settings_provider.dart"
|
|
provides: "Theme and unit state management with persistence"
|
|
contains: "class SettingsProvider extends ChangeNotifier"
|
|
- path: "lib/providers/overlay_provider.dart"
|
|
provides: "Overlay lifecycle and messaging centralization"
|
|
contains: "class OverlayProvider extends ChangeNotifier"
|
|
- path: "pubspec.yaml"
|
|
provides: "Updated dependencies"
|
|
contains: "provider:"
|
|
key_links:
|
|
- from: "lib/services/gps_data_manager.dart"
|
|
to: "ChangeNotifier"
|
|
via: "extends mixin, notifyListeners() in _updateData()"
|
|
pattern: "notifyListeners"
|
|
- from: "lib/providers/settings_provider.dart"
|
|
to: "SharedPreferences"
|
|
via: "constructor injection, sync load"
|
|
pattern: "SharedPreferences"
|
|
- from: "lib/providers/overlay_provider.dart"
|
|
to: "FlutterOverlayWindow.shareData"
|
|
via: "_pushToOverlay method"
|
|
pattern: "shareData"
|
|
---
|
|
|
|
<objective>
|
|
Create the three Provider classes (GpsDataManager refactor, SettingsProvider, OverlayProvider) and add required dependencies. These providers encapsulate all shared app state that was previously managed by setState in SpeedometerScreen.
|
|
|
|
Purpose: Establish the Provider infrastructure so Plan 02 can wire them into the widget tree and migrate the screen.
|
|
Output: Three ChangeNotifier classes ready for MultiProvider wiring, plus updated pubspec.yaml with provider and shared_preferences packages.
|
|
</objective>
|
|
|
|
<execution_context>
|
|
@C:\Users\SKSup\.claude/get-shit-done/workflows/execute-plan.md
|
|
@C:\Users\SKSup\.claude/get-shit-done/templates/summary.md
|
|
</execution_context>
|
|
|
|
<context>
|
|
@.planning/PROJECT.md
|
|
@.planning/ROADMAP.md
|
|
@.planning/STATE.md
|
|
@.planning/phases/02-provider-migration/02-CONTEXT.md
|
|
@.planning/phases/02-provider-migration/02-RESEARCH.md
|
|
@lib/services/gps_data_manager.dart
|
|
@lib/config/speed_unit.dart
|
|
@lib/config/color_themes.dart
|
|
@lib/config/overlay_constants.dart
|
|
@lib/config/timing_constants.dart
|
|
@lib/models/overlay_message.dart
|
|
@lib/models/processed_gps_data.dart
|
|
@lib/services/logger.dart
|
|
@pubspec.yaml
|
|
</context>
|
|
|
|
<tasks>
|
|
|
|
<task type="auto">
|
|
<name>Task 1: Add dependencies and refactor GpsDataManager to ChangeNotifier</name>
|
|
<files>pubspec.yaml, lib/services/gps_data_manager.dart</files>
|
|
<action>
|
|
**pubspec.yaml:**
|
|
Add two dependencies under the existing dependencies section:
|
|
```yaml
|
|
# State management
|
|
provider: ^6.1.2
|
|
|
|
# Persistent settings storage
|
|
shared_preferences: ^2.3.4
|
|
```
|
|
|
|
Run `flutter pub get` to install.
|
|
|
|
**lib/services/gps_data_manager.dart:**
|
|
Refactor the existing GpsDataManager class. The file stays in `services/` (it's a service that happens to be observable, not a pure provider).
|
|
|
|
Changes:
|
|
1. Add `import 'package:flutter/foundation.dart';` for ChangeNotifier
|
|
2. Change class declaration from `class GpsDataManager` to `class GpsDataManager extends ChangeNotifier`
|
|
3. **Remove the singleton pattern entirely:**
|
|
- Delete `static GpsDataManager? _instance;`
|
|
- Delete `static GpsDataManager get instance => _instance ??= GpsDataManager._internal();`
|
|
- Rename `GpsDataManager._internal()` to just `GpsDataManager()` (public constructor)
|
|
4. **Add _isDisposed guard** to prevent "ChangeNotifier after dispose" errors:
|
|
- Add field: `bool _isDisposed = false;`
|
|
- In `_updateData()`: Add `if (_isDisposed) return;` as the first line, then after updating `_currentData` and adding to stream, call `notifyListeners();`
|
|
- In `_onGpsError()`: Add `if (_isDisposed) return;` as the first line
|
|
- In `_handleStaleDataTimeout()`: Add `if (_isDisposed) return;` as the first line
|
|
5. **Update dispose()** to be a proper ChangeNotifier override:
|
|
- Add `@override` annotation
|
|
- Set `_isDisposed = true;` as the FIRST line
|
|
- Keep existing cleanup (cancel subscription, cancel timer, close stream controller)
|
|
- Call `super.dispose();` as the LAST line (replaces setting `_isInitialized = false`)
|
|
6. **Add notifyListeners() to _updateData():**
|
|
```dart
|
|
void _updateData(ProcessedGpsData newData) {
|
|
if (_isDisposed) return;
|
|
Logger.debug('Broadcasting data: ${newData.displaySpeed} ${newData.displayHeading}', 'GpsDataManager');
|
|
_currentData = newData;
|
|
_dataController.add(newData);
|
|
notifyListeners();
|
|
Logger.debug('Data broadcast complete', 'GpsDataManager');
|
|
}
|
|
```
|
|
|
|
Do NOT change: initialize(), _onPositionUpdate(), _scheduleStaleDataTimer(), getFormattedSpeed(), dataStream getter, currentData getter. The existing stream-based API stays alongside ChangeNotifier for OverlayProvider's stream subscription.
|
|
</action>
|
|
<verify>
|
|
Run `flutter pub get` — must succeed with no version conflicts.
|
|
Run `flutter analyze` — no errors (warnings about SCREAMING_SNAKE are expected and acceptable).
|
|
Verify GpsDataManager has no `static` instance or `_instance` field.
|
|
Verify `extends ChangeNotifier` is in the class declaration.
|
|
Verify `notifyListeners()` is called in `_updateData()`.
|
|
Verify `_isDisposed` guard exists in `_updateData`, `_onGpsError`, `_handleStaleDataTimeout`.
|
|
Verify `dispose()` calls `super.dispose()` as last line.
|
|
</verify>
|
|
<done>
|
|
GpsDataManager is a proper ChangeNotifier with public constructor, no singleton, _isDisposed safety guard, and notifyListeners() on every data update. Dependencies installed.
|
|
</done>
|
|
</task>
|
|
|
|
<task type="auto">
|
|
<name>Task 2: Create SettingsProvider for theme and unit state</name>
|
|
<files>lib/providers/settings_provider.dart</files>
|
|
<action>
|
|
Create the `lib/providers/` directory and create `settings_provider.dart`.
|
|
|
|
SettingsProvider manages theme index and speed unit, persisted to SharedPreferences. It replaces the `_currentUnit` and `_currentThemeIndex` state variables from SpeedometerScreen, plus the `_cycleUnit()` and `_cycleTheme()` methods.
|
|
|
|
```dart
|
|
import 'package:flutter/foundation.dart';
|
|
import 'package:flutter/services.dart';
|
|
import 'package:shared_preferences/shared_preferences.dart';
|
|
import '../config/speed_unit.dart';
|
|
import '../config/color_themes.dart';
|
|
import '../services/logger.dart';
|
|
|
|
class SettingsProvider extends ChangeNotifier {
|
|
final SharedPreferences _prefs;
|
|
|
|
int _currentThemeIndex = 0;
|
|
SpeedUnit _currentUnit = SpeedUnit.kmh;
|
|
|
|
// Getters for read access
|
|
int get currentThemeIndex => _currentThemeIndex;
|
|
SpeedUnit get currentUnit => _currentUnit;
|
|
ColorTheme get currentTheme => ColorThemes.getTheme(_currentThemeIndex);
|
|
|
|
/// Constructor loads settings synchronously from pre-initialized SharedPreferences.
|
|
/// SharedPreferences MUST be initialized before constructing this provider.
|
|
SettingsProvider(this._prefs) {
|
|
_loadSettings();
|
|
Logger.info('Settings loaded: theme=$_currentThemeIndex, unit=${_currentUnit.label}', 'SettingsProvider');
|
|
}
|
|
|
|
void _loadSettings() {
|
|
_currentThemeIndex = _prefs.getInt('themeIndex') ?? 0;
|
|
final unitIndex = _prefs.getInt('unitIndex') ?? 0;
|
|
_currentUnit = SpeedUnit.values[unitIndex.clamp(0, SpeedUnit.values.length - 1)];
|
|
// No notifyListeners() — constructor runs before first build
|
|
}
|
|
|
|
/// Cycle to next theme with haptic feedback and persistence
|
|
void cycleTheme() {
|
|
HapticFeedback.lightImpact();
|
|
_currentThemeIndex = ColorThemes.getNextThemeIndex(_currentThemeIndex);
|
|
_prefs.setInt('themeIndex', _currentThemeIndex);
|
|
Logger.debug('Theme cycled to index $_currentThemeIndex', 'SettingsProvider');
|
|
notifyListeners();
|
|
}
|
|
|
|
/// Cycle to next speed unit with haptic feedback and persistence
|
|
void cycleUnit() {
|
|
HapticFeedback.lightImpact();
|
|
_currentUnit = _currentUnit.next;
|
|
_prefs.setInt('unitIndex', _currentUnit.index);
|
|
Logger.debug('Unit cycled to ${_currentUnit.label}', 'SettingsProvider');
|
|
notifyListeners();
|
|
}
|
|
}
|
|
```
|
|
|
|
Key design decisions:
|
|
- SharedPreferences injected via constructor (pre-initialized in main())
|
|
- `_loadSettings()` is synchronous — reads from already-loaded SharedPreferences instance
|
|
- Haptic feedback included in cycle methods (moved from SpeedometerScreen)
|
|
- `currentTheme` convenience getter avoids repeated `ColorThemes.getTheme()` calls in UI
|
|
- `unitIndex.clamp()` prevents crash if persisted value is out of range
|
|
- No dispose override needed — no streams or timers to clean up
|
|
</action>
|
|
<verify>
|
|
Run `flutter analyze` — no errors in the new file.
|
|
Verify SettingsProvider extends ChangeNotifier.
|
|
Verify constructor takes SharedPreferences parameter.
|
|
Verify cycleTheme() and cycleUnit() both call notifyListeners() and persist to SharedPreferences.
|
|
Verify _loadSettings() does NOT call notifyListeners().
|
|
</verify>
|
|
<done>
|
|
SettingsProvider manages theme and unit state with SharedPreferences persistence, haptic feedback on changes, and ChangeNotifier notifications.
|
|
</done>
|
|
</task>
|
|
|
|
<task type="auto">
|
|
<name>Task 3: Create OverlayProvider for overlay lifecycle and messaging</name>
|
|
<files>lib/providers/overlay_provider.dart</files>
|
|
<action>
|
|
Create `lib/providers/overlay_provider.dart`.
|
|
|
|
OverlayProvider centralizes ALL overlay management that was scattered across SpeedometerScreen: overlay show/close, message listening, status checking, data pushing, and tap-close handling. It listens to GpsDataManager's stream continuously and skips sends when overlay is inactive (if-check, not unsubscribe — per context decision).
|
|
|
|
```dart
|
|
import 'dart:async';
|
|
import 'package:flutter/foundation.dart';
|
|
import 'package:flutter/widgets.dart';
|
|
import 'package:flutter_overlay_window/flutter_overlay_window.dart';
|
|
import 'package:bg_launcher/bg_launcher.dart';
|
|
import '../config/overlay_constants.dart';
|
|
import '../config/timing_constants.dart';
|
|
import '../config/speed_unit.dart';
|
|
import '../models/overlay_message.dart';
|
|
import '../models/processed_gps_data.dart';
|
|
import '../services/gps_data_manager.dart';
|
|
import '../services/logger.dart';
|
|
|
|
class OverlayProvider extends ChangeNotifier {
|
|
bool _isOverlayActive = false;
|
|
bool _isDisposed = false;
|
|
bool _tapCloseRequested = false;
|
|
|
|
StreamSubscription<ProcessedGpsData>? _gpsSubscription;
|
|
StreamSubscription<dynamic>? _overlayMessageSubscription;
|
|
Timer? _overlayStatusCheckTimer;
|
|
Timer? _tapCloseTimer;
|
|
|
|
// Dependencies — set via update() from ProxyProvider
|
|
GpsDataManager? _gpsManager;
|
|
SpeedUnit _currentUnit = SpeedUnit.kmh;
|
|
int _currentThemeIndex = 0;
|
|
|
|
bool get isOverlayActive => _isOverlayActive;
|
|
|
|
/// Called by ChangeNotifierProxyProvider2 when GpsDataManager or SettingsProvider changes.
|
|
/// Sets up GPS stream subscription on first call, updates settings references on subsequent calls.
|
|
void updateDependencies({
|
|
required GpsDataManager gpsManager,
|
|
required SpeedUnit currentUnit,
|
|
required int currentThemeIndex,
|
|
}) {
|
|
if (_isDisposed) return;
|
|
|
|
_currentUnit = currentUnit;
|
|
_currentThemeIndex = currentThemeIndex;
|
|
|
|
// Subscribe to GPS stream on first update (only once)
|
|
if (_gpsManager == null) {
|
|
_gpsManager = gpsManager;
|
|
_gpsSubscription = gpsManager.dataStream.listen((data) {
|
|
if (_isOverlayActive && !_isDisposed) {
|
|
_pushDataToOverlay(data);
|
|
}
|
|
});
|
|
Logger.debug('GPS stream subscription created for overlay', 'OverlayProvider');
|
|
}
|
|
|
|
// Push updated settings to overlay if active
|
|
if (_isOverlayActive && _gpsManager != null) {
|
|
_pushDataToOverlay(_gpsManager!.currentData);
|
|
}
|
|
}
|
|
|
|
/// Show the floating overlay window
|
|
Future<void> showOverlay() async {
|
|
if (_isDisposed) return;
|
|
|
|
try {
|
|
final overlaySize = _getOverlaySize();
|
|
final gpsData = _gpsManager?.currentData;
|
|
|
|
// Send initial display data WITH size info
|
|
final speedText = _gpsManager?.getFormattedSpeed(_currentUnit) ?? '--';
|
|
final headingText = gpsData?.displayHeading ?? 'N/A';
|
|
final unitText = _currentUnit.label;
|
|
|
|
await FlutterOverlayWindow.shareData(OverlayMessage.updateDisplay(
|
|
speedText: speedText,
|
|
unitText: unitText,
|
|
headingText: headingText,
|
|
heading: gpsData?.heading ?? -1.0,
|
|
unitIndex: _currentUnit.index,
|
|
themeIndex: _currentThemeIndex,
|
|
overlayWidth: overlaySize['width']!.toDouble(),
|
|
overlayHeight: overlaySize['height']!.toDouble(),
|
|
).toMap());
|
|
|
|
await FlutterOverlayWindow.showOverlay(
|
|
enableDrag: true,
|
|
overlayTitle: "Speedometer",
|
|
overlayContent: 'Speedo overlay active',
|
|
flag: OverlayFlag.defaultFlag,
|
|
visibility: NotificationVisibility.visibilityPublic,
|
|
positionGravity: PositionGravity.none,
|
|
width: overlaySize['width']!,
|
|
height: overlaySize['height']!,
|
|
);
|
|
|
|
Logger.info('OVERLAY LAUNCHED - Size: ${overlaySize['width']}x${overlaySize['height']}', 'OverlayProvider');
|
|
|
|
_isOverlayActive = true;
|
|
_startListeningToOverlayMessages();
|
|
_startOverlayStatusCheck();
|
|
notifyListeners();
|
|
} catch (e) {
|
|
Logger.error('Failed to show overlay: $e', 'OverlayProvider');
|
|
}
|
|
}
|
|
|
|
/// Close the floating overlay window
|
|
Future<void> closeOverlay() async {
|
|
if (!_isOverlayActive || _isDisposed) return;
|
|
|
|
try {
|
|
await FlutterOverlayWindow.closeOverlay();
|
|
Logger.info('OVERLAY CLOSED - Requested from main app', 'OverlayProvider');
|
|
_handleOverlayClose();
|
|
} catch (e) {
|
|
Logger.error('Error closing overlay: $e', 'OverlayProvider');
|
|
}
|
|
}
|
|
|
|
/// Toggle overlay state (show if hidden, close if active)
|
|
Future<void> toggleOverlay() async {
|
|
if (_isOverlayActive) {
|
|
await closeOverlay();
|
|
} else {
|
|
await showOverlay();
|
|
}
|
|
}
|
|
|
|
Map<String, int> _getOverlaySize() {
|
|
final window = WidgetsBinding.instance.platformDispatcher.views.first;
|
|
final physicalSize = window.physicalSize;
|
|
final overlayWidth = (physicalSize.width * OverlayConfig.WIDTH_PERCENTAGE).round();
|
|
final overlayHeight = (overlayWidth * OverlayConfig.ASPECT_RATIO).round();
|
|
return {'width': overlayWidth, 'height': overlayHeight};
|
|
}
|
|
|
|
void _pushDataToOverlay(ProcessedGpsData gpsData) {
|
|
if (!_isOverlayActive || _isDisposed) return;
|
|
|
|
final speedText = _gpsManager?.getFormattedSpeed(_currentUnit) ?? '--';
|
|
final unitText = _currentUnit.label;
|
|
final headingText = gpsData.displayHeading;
|
|
|
|
Logger.debug('Pushing to overlay: speed="$speedText", heading="$headingText"', 'OverlayProvider');
|
|
|
|
FlutterOverlayWindow.shareData(OverlayMessage.updateDisplay(
|
|
speedText: speedText,
|
|
unitText: unitText,
|
|
headingText: headingText,
|
|
heading: gpsData.heading,
|
|
unitIndex: _currentUnit.index,
|
|
themeIndex: _currentThemeIndex,
|
|
).toMap());
|
|
}
|
|
|
|
void _startListeningToOverlayMessages() {
|
|
if (_overlayMessageSubscription != null) return;
|
|
|
|
_overlayMessageSubscription = FlutterOverlayWindow.overlayListener.listen(
|
|
(data) {
|
|
try {
|
|
if (data is Map) {
|
|
final action = data['action'];
|
|
switch (action) {
|
|
case 'overlayClosed':
|
|
_handleOverlayClose();
|
|
break;
|
|
case 'longPressClose':
|
|
_tapCloseRequested = true;
|
|
_tapCloseTimer?.cancel();
|
|
_tapCloseTimer = Timer(TimingConfig.TAP_CLOSE_DELAY, () {
|
|
_tapCloseRequested = false;
|
|
});
|
|
break;
|
|
default:
|
|
Logger.warn('Unknown overlay action: "$action"', 'OverlayProvider');
|
|
}
|
|
}
|
|
} catch (e) {
|
|
Logger.error('Error processing overlay message: $e', 'OverlayProvider');
|
|
}
|
|
},
|
|
onError: (error) {
|
|
Logger.error('Overlay listener error: $error', 'OverlayProvider');
|
|
},
|
|
);
|
|
}
|
|
|
|
void _stopListeningToOverlayMessages() {
|
|
_overlayMessageSubscription?.cancel();
|
|
_overlayMessageSubscription = null;
|
|
}
|
|
|
|
void _startOverlayStatusCheck() {
|
|
_overlayStatusCheckTimer?.cancel();
|
|
|
|
_overlayStatusCheckTimer = Timer.periodic(TimingConfig.OVERLAY_STATUS_CHECK_INTERVAL, (timer) async {
|
|
if (_isOverlayActive && !_isDisposed) {
|
|
try {
|
|
final isActive = await FlutterOverlayWindow.isActive();
|
|
if (!isActive) {
|
|
timer.cancel();
|
|
_handleOverlayClose();
|
|
}
|
|
} catch (e) {
|
|
Logger.error('Overlay status check failed: $e', 'OverlayProvider');
|
|
}
|
|
} else {
|
|
timer.cancel();
|
|
}
|
|
});
|
|
Logger.debug('Started overlay status monitoring', 'OverlayProvider');
|
|
}
|
|
|
|
void _handleOverlayClose({bool bringToForeground = false}) {
|
|
if (_isDisposed) return;
|
|
|
|
final shouldBringToFront = bringToForeground || _tapCloseRequested;
|
|
|
|
_tapCloseTimer?.cancel();
|
|
_overlayStatusCheckTimer?.cancel();
|
|
_tapCloseRequested = false;
|
|
|
|
_isOverlayActive = false;
|
|
_stopListeningToOverlayMessages();
|
|
notifyListeners();
|
|
|
|
if (shouldBringToFront) {
|
|
_bringAppToFront();
|
|
}
|
|
}
|
|
|
|
void _bringAppToFront() {
|
|
try {
|
|
BgLauncher.bringAppToForeground();
|
|
} catch (e) {
|
|
Logger.error('BgLauncher failed to bring app to front: $e', 'OverlayProvider');
|
|
}
|
|
}
|
|
|
|
/// Push current data to overlay — used by background heartbeat
|
|
void pushCurrentData() {
|
|
if (_isOverlayActive && _gpsManager != null && !_isDisposed) {
|
|
_pushDataToOverlay(_gpsManager!.currentData);
|
|
}
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_isDisposed = true;
|
|
_gpsSubscription?.cancel();
|
|
_overlayMessageSubscription?.cancel();
|
|
_overlayStatusCheckTimer?.cancel();
|
|
_tapCloseTimer?.cancel();
|
|
Logger.info('OverlayProvider disposed', 'OverlayProvider');
|
|
super.dispose();
|
|
}
|
|
}
|
|
```
|
|
|
|
Key design decisions per context:
|
|
- GPS stream subscription created once in updateDependencies(), never torn down (skips sends via if-check when inactive)
|
|
- `_isDisposed` guard on all public methods and async callbacks
|
|
- Heartbeat/polling stripped down to simple boolean `_isOverlayActive` — Phase 3 builds proper detection
|
|
- All overlay message handling, status checking, and close logic moved from SpeedometerScreen
|
|
- `_bringAppToFront()` simplified — no setState needed since Provider handles state
|
|
- `pushCurrentData()` exposed for background heartbeat timer (stays in SpeedometerScreen as local widget concern)
|
|
- `toggleOverlay()` convenience method for the compass area tap handler
|
|
</action>
|
|
<verify>
|
|
Run `flutter analyze` — no errors in the new file.
|
|
Verify OverlayProvider extends ChangeNotifier.
|
|
Verify _isDisposed guard exists in showOverlay, closeOverlay, updateDependencies, _pushDataToOverlay, _handleOverlayClose.
|
|
Verify dispose() cancels all 4 subscriptions/timers and calls super.dispose() last.
|
|
Verify _pushDataToOverlay checks `_isOverlayActive` before sending.
|
|
</verify>
|
|
<done>
|
|
OverlayProvider centralizes all overlay lifecycle management with GPS stream listening, overlay messaging, status checking, and proper disposal. Ready for MultiProvider wiring.
|
|
</done>
|
|
</task>
|
|
|
|
</tasks>
|
|
|
|
<verification>
|
|
After all 3 tasks complete:
|
|
1. `flutter pub get` succeeds — provider and shared_preferences installed
|
|
2. `flutter analyze` shows no errors (SCREAMING_SNAKE warnings expected)
|
|
3. GpsDataManager: no singleton, extends ChangeNotifier, has notifyListeners() in _updateData()
|
|
4. SettingsProvider: loads from SharedPreferences, persists on change, has cycleTheme/cycleUnit
|
|
5. OverlayProvider: manages overlay lifecycle, listens to GPS stream, skips sends when inactive
|
|
6. All three classes have proper dispose() with super.dispose() called last
|
|
7. All async-accessible methods guarded with _isDisposed check
|
|
</verification>
|
|
|
|
<success_criteria>
|
|
- Three ChangeNotifier classes exist and compile without errors
|
|
- GpsDataManager singleton pattern removed, replaced with public constructor
|
|
- SettingsProvider reads and writes SharedPreferences for theme and unit
|
|
- OverlayProvider encapsulates all overlay management from SpeedometerScreen
|
|
- Dependencies (provider, shared_preferences) added to pubspec.yaml and resolvable
|
|
- No circular dependencies between provider classes
|
|
</success_criteria>
|
|
|
|
<output>
|
|
After completion, create `.planning/phases/02-provider-migration/02-01-SUMMARY.md`
|
|
</output>
|