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
568 lines
21 KiB
Markdown
568 lines
21 KiB
Markdown
---
|
|
phase: 02-provider-migration
|
|
plan: 02
|
|
type: execute
|
|
wave: 2
|
|
depends_on: ["02-01"]
|
|
files_modified:
|
|
- lib/main.dart
|
|
- lib/screens/speedometer_screen.dart
|
|
autonomous: true
|
|
|
|
must_haves:
|
|
truths:
|
|
- "App launches with pre-initialized SharedPreferences — no flash of default values"
|
|
- "Speed display updates reactively from GpsDataManager via Selector without rebuilding theme or heading"
|
|
- "Tapping speed text cycles theme via SettingsProvider (not setState)"
|
|
- "Tapping unit label cycles unit via SettingsProvider (not setState)"
|
|
- "Tapping compass area toggles overlay via OverlayProvider (not setState)"
|
|
- "No setState calls remain for shared app state — only local widget state (if any)"
|
|
- "App compiles and runs with identical visual behavior to pre-migration"
|
|
- "Background heartbeat and lifecycle observer still function"
|
|
artifacts:
|
|
- path: "lib/main.dart"
|
|
provides: "MultiProvider wiring with pre-initialized SharedPreferences"
|
|
contains: "MultiProvider"
|
|
- path: "lib/screens/speedometer_screen.dart"
|
|
provides: "Provider-consuming speedometer screen with Consumer/Selector widgets"
|
|
contains: "Selector<GpsDataManager"
|
|
key_links:
|
|
- from: "lib/main.dart"
|
|
to: "lib/services/gps_data_manager.dart"
|
|
via: "ChangeNotifierProvider create callback"
|
|
pattern: "ChangeNotifierProvider"
|
|
- from: "lib/main.dart"
|
|
to: "lib/providers/settings_provider.dart"
|
|
via: "ChangeNotifierProvider with SharedPreferences injection"
|
|
pattern: "SettingsProvider"
|
|
- from: "lib/main.dart"
|
|
to: "lib/providers/overlay_provider.dart"
|
|
via: "ChangeNotifierProxyProvider2"
|
|
pattern: "ChangeNotifierProxyProvider2"
|
|
- from: "lib/screens/speedometer_screen.dart"
|
|
to: "lib/services/gps_data_manager.dart"
|
|
via: "Selector widget for speed/heading data"
|
|
pattern: "Selector<GpsDataManager"
|
|
- from: "lib/screens/speedometer_screen.dart"
|
|
to: "lib/providers/settings_provider.dart"
|
|
via: "context.read in event handlers, Consumer in build"
|
|
pattern: "context.read<SettingsProvider>"
|
|
- from: "lib/screens/speedometer_screen.dart"
|
|
to: "lib/providers/overlay_provider.dart"
|
|
via: "context.read in event handlers, Selector for isOverlayActive"
|
|
pattern: "context.read<OverlayProvider>"
|
|
---
|
|
|
|
<objective>
|
|
Wire all three providers into the widget tree via MultiProvider in main.dart, then fully migrate SpeedometerScreen from setState to Consumer/Selector pattern. This is the "big bang" migration — all shared state moves to Provider in one pass.
|
|
|
|
Purpose: Complete the setState-to-Provider migration so the app has reactive state management with granular rebuilds.
|
|
Output: Working app with MultiProvider at root, SpeedometerScreen consuming providers via Consumer/Selector, zero setState calls for shared state.
|
|
</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
|
|
@.planning/phases/02-provider-migration/02-01-SUMMARY.md
|
|
@lib/main.dart
|
|
@lib/screens/speedometer_screen.dart
|
|
@lib/services/gps_data_manager.dart
|
|
@lib/providers/settings_provider.dart
|
|
@lib/providers/overlay_provider.dart
|
|
@lib/config/speed_unit.dart
|
|
@lib/config/color_themes.dart
|
|
</context>
|
|
|
|
<tasks>
|
|
|
|
<task type="auto">
|
|
<name>Task 1: Wire MultiProvider in main.dart</name>
|
|
<files>lib/main.dart</files>
|
|
<action>
|
|
Rewrite main.dart to set up MultiProvider with pre-initialized SharedPreferences. The file should remain a clean entry point (target ~50-60 lines).
|
|
|
|
**Changes to main():**
|
|
1. Make main() async
|
|
2. Call `WidgetsFlutterBinding.ensureInitialized()` before any async work
|
|
3. Pre-initialize SharedPreferences: `final prefs = await SharedPreferences.getInstance();`
|
|
4. Wrap `SpeedoApp` in `MultiProvider` with providers in dependency order
|
|
|
|
**Provider wiring order (dependencies first):**
|
|
```dart
|
|
MultiProvider(
|
|
providers: [
|
|
// 1. Raw dependencies (no ChangeNotifier)
|
|
Provider<SharedPreferences>.value(value: prefs),
|
|
|
|
// 2. Independent ChangeNotifiers
|
|
ChangeNotifierProvider(
|
|
create: (context) => SettingsProvider(context.read<SharedPreferences>()),
|
|
),
|
|
ChangeNotifierProvider(
|
|
create: (_) => GpsDataManager(),
|
|
),
|
|
|
|
// 3. Dependent ChangeNotifier (depends on GPS + Settings)
|
|
ChangeNotifierProxyProvider2<GpsDataManager, SettingsProvider, OverlayProvider>(
|
|
create: (_) => OverlayProvider(),
|
|
update: (_, gpsManager, settings, overlay) => overlay!
|
|
..updateDependencies(
|
|
gpsManager: gpsManager,
|
|
currentUnit: settings.currentUnit,
|
|
currentThemeIndex: settings.currentThemeIndex,
|
|
),
|
|
),
|
|
],
|
|
child: const SpeedoApp(),
|
|
)
|
|
```
|
|
|
|
**Changes to SpeedoApp:**
|
|
- Remove `isOverlayMode` parameter — no longer needed (overlay runs in separate isolate with its own entry point)
|
|
- Theme is now derived from SettingsProvider in the screen, not hardcoded in SpeedoApp
|
|
- Keep the MaterialApp with `useMaterial3: true` and base dark theme as fallback
|
|
|
|
**overlayMain() stays unchanged** — overlay runs in separate isolate and cannot access Provider.
|
|
|
|
**Required imports:**
|
|
```dart
|
|
import 'package:provider/provider.dart';
|
|
import 'package:shared_preferences/shared_preferences.dart';
|
|
import 'services/gps_data_manager.dart';
|
|
import 'providers/settings_provider.dart';
|
|
import 'providers/overlay_provider.dart';
|
|
```
|
|
|
|
**Full target main.dart structure:**
|
|
```dart
|
|
import 'package:flutter/material.dart';
|
|
import 'package:provider/provider.dart';
|
|
import 'package:shared_preferences/shared_preferences.dart';
|
|
import 'screens/speedometer_screen.dart';
|
|
import 'screens/overlay_screen.dart';
|
|
import 'services/gps_data_manager.dart';
|
|
import 'providers/settings_provider.dart';
|
|
import 'providers/overlay_provider.dart';
|
|
import 'services/logger.dart';
|
|
|
|
void main() async {
|
|
WidgetsFlutterBinding.ensureInitialized();
|
|
final prefs = await SharedPreferences.getInstance();
|
|
|
|
runApp(
|
|
MultiProvider(
|
|
providers: [
|
|
Provider<SharedPreferences>.value(value: prefs),
|
|
ChangeNotifierProvider(
|
|
create: (context) => SettingsProvider(context.read<SharedPreferences>()),
|
|
),
|
|
ChangeNotifierProvider(
|
|
create: (_) => GpsDataManager(),
|
|
),
|
|
ChangeNotifierProxyProvider2<GpsDataManager, SettingsProvider, OverlayProvider>(
|
|
create: (_) => OverlayProvider(),
|
|
update: (_, gpsManager, settings, overlay) => overlay!
|
|
..updateDependencies(
|
|
gpsManager: gpsManager,
|
|
currentUnit: settings.currentUnit,
|
|
currentThemeIndex: settings.currentThemeIndex,
|
|
),
|
|
),
|
|
],
|
|
child: const SpeedoApp(),
|
|
),
|
|
);
|
|
}
|
|
|
|
@pragma("vm:entry-point")
|
|
void overlayMain() {
|
|
Logger.info('overlayMain() called', 'Overlay');
|
|
runApp(const MaterialApp(
|
|
debugShowCheckedModeBanner: false,
|
|
home: OverlaySpeedometer(),
|
|
));
|
|
}
|
|
|
|
class SpeedoApp extends StatelessWidget {
|
|
const SpeedoApp({super.key});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return MaterialApp(
|
|
title: 'Speedo',
|
|
debugShowCheckedModeBanner: false,
|
|
theme: ThemeData(
|
|
useMaterial3: true,
|
|
brightness: Brightness.dark,
|
|
colorSchemeSeed: Colors.green,
|
|
),
|
|
home: const SpeedometerScreen(),
|
|
);
|
|
}
|
|
}
|
|
```
|
|
</action>
|
|
<verify>
|
|
Run `flutter analyze` — no errors.
|
|
Verify main() is async with WidgetsFlutterBinding.ensureInitialized().
|
|
Verify MultiProvider wraps SpeedoApp with all 4 providers.
|
|
Verify provider order: SharedPreferences first, then SettingsProvider, GpsDataManager, OverlayProvider.
|
|
Verify ChangeNotifierProxyProvider2 passes dependencies to OverlayProvider.updateDependencies().
|
|
Verify overlayMain() is unchanged.
|
|
Verify SpeedoApp no longer has isOverlayMode parameter.
|
|
</verify>
|
|
<done>
|
|
main.dart wires MultiProvider with pre-initialized SharedPreferences, three ChangeNotifier providers in correct dependency order, and OverlayProvider receiving proxy updates from GPS and Settings.
|
|
</done>
|
|
</task>
|
|
|
|
<task type="auto">
|
|
<name>Task 2: Migrate SpeedometerScreen from setState to Provider</name>
|
|
<files>lib/screens/speedometer_screen.dart</files>
|
|
<action>
|
|
Full rewrite of SpeedometerScreen to consume providers via Consumer/Selector. This is the core of the "big bang migration."
|
|
|
|
**State variables to REMOVE (now in providers):**
|
|
- `_currentUnit` — now in SettingsProvider
|
|
- `_currentThemeIndex` — now in SettingsProvider
|
|
- `_gpsDataSubscription` — GPS subscription now in GpsDataManager (Provider-managed lifecycle)
|
|
- `_overlaySubscription` — now in OverlayProvider
|
|
- `_currentGpsData` — accessed via `context.watch<GpsDataManager>().currentData`
|
|
- `_isOverlayActive` — now in OverlayProvider
|
|
- `_tapCloseRequested` — now in OverlayProvider
|
|
- `_tapCloseTimer` — now in OverlayProvider
|
|
- `_overlayStatusCheckTimer` — now in OverlayProvider
|
|
|
|
**State variables to KEEP (local widget state):**
|
|
- `_errorMessage` — local UI state for GPS init errors
|
|
- `_isInBackground` — local lifecycle tracking for heartbeat
|
|
- `_backgroundHeartbeatTimer` — local timer for background keep-alive
|
|
|
|
**Widget parameter to REMOVE:**
|
|
- `isOverlayMode` — not used (overlay has separate entry point)
|
|
|
|
**Methods to REMOVE (now in providers):**
|
|
- `_cycleUnit()` — replaced by `context.read<SettingsProvider>().cycleUnit()`
|
|
- `_cycleTheme()` — replaced by `context.read<SettingsProvider>().cycleTheme()`
|
|
- `_pushDataToOverlay()` — now in OverlayProvider
|
|
- `_showFloatingWindow()` — replaced by `context.read<OverlayProvider>().showOverlay()`
|
|
- `_closeFloatingWindow()` — replaced by `context.read<OverlayProvider>().closeOverlay()`
|
|
- `_startListeningToOverlayMessages()` — now in OverlayProvider
|
|
- `_stopListeningToOverlayMessages()` — now in OverlayProvider
|
|
- `_startOverlayStatusCheck()` — now in OverlayProvider
|
|
- `_handleOverlayClose()` — now in OverlayProvider
|
|
- `_bringAppToFront()` — now in OverlayProvider
|
|
- `_getOverlaySize()` — now in OverlayProvider
|
|
|
|
**Methods to KEEP (local concerns):**
|
|
- `_initializeGpsManager()` — simplified: calls `context.read<GpsDataManager>().initialize()`, handles error with local `_errorMessage` setState
|
|
- `_enableWakelock()` — platform service, not state
|
|
- `_ensureOverlayPermission()` — platform service, not state
|
|
- `_startBackgroundHeartbeat()` — local timer, uses `context.read<OverlayProvider>().pushCurrentData()`
|
|
- `_handleBackgroundTransition()` — local lifecycle
|
|
- `_requestBatteryOptimizationExemption()` — platform service
|
|
- `didChangeAppLifecycleState()` — local lifecycle observer
|
|
- `_getSpeedDisplayText()` — pure function
|
|
- `_buildSpeedDisplay()` — pure UI builder
|
|
- `_buildPortraitLayout()` — UI layout (now receives data from Provider)
|
|
- `_buildLandscapeLayout()` — UI layout (now receives data from Provider)
|
|
|
|
**initState changes:**
|
|
```dart
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
WidgetsBinding.instance.addObserver(this);
|
|
// Delay GPS init to after first frame so context is available
|
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
_initializeGpsManager();
|
|
});
|
|
_enableWakelock();
|
|
_ensureOverlayPermission();
|
|
_startBackgroundHeartbeat();
|
|
}
|
|
```
|
|
|
|
**dispose changes:**
|
|
```dart
|
|
@override
|
|
void dispose() {
|
|
WidgetsBinding.instance.removeObserver(this);
|
|
_backgroundHeartbeatTimer?.cancel();
|
|
WakelockPlus.disable();
|
|
Logger.info('SpeedometerScreen disposed', 'Main');
|
|
super.dispose();
|
|
}
|
|
```
|
|
Note: Much simpler — GPS subscription, overlay subscription, overlay status timer, tap close timer all managed by providers now.
|
|
|
|
**_initializeGpsManager changes:**
|
|
```dart
|
|
Future<void> _initializeGpsManager() async {
|
|
Logger.info('Starting GPS manager initialization...', 'Main');
|
|
try {
|
|
await context.read<GpsDataManager>().initialize();
|
|
Logger.info('GPS manager initialized', 'Main');
|
|
} catch (e) {
|
|
Logger.error('GPS manager initialization failed: $e', 'Main');
|
|
if (mounted) {
|
|
setState(() => _errorMessage = 'Failed to initialize GPS manager');
|
|
}
|
|
}
|
|
}
|
|
```
|
|
Note: `_errorMessage` is the ONLY remaining setState — it's purely local error UI state.
|
|
|
|
**_startBackgroundHeartbeat changes:**
|
|
```dart
|
|
void _startBackgroundHeartbeat() {
|
|
_backgroundHeartbeatTimer = Timer.periodic(TimingConfig.HEARTBEAT_INTERVAL, (timer) {
|
|
if (!mounted) return;
|
|
final overlay = context.read<OverlayProvider>();
|
|
if (_isInBackground && overlay.isOverlayActive) {
|
|
overlay.pushCurrentData();
|
|
}
|
|
});
|
|
}
|
|
```
|
|
Note: Simplified — no more fake setState to keep engine alive. Just push data to overlay.
|
|
|
|
**didChangeAppLifecycleState changes:**
|
|
Keep the same structure but remove setState for _isInBackground (it's purely local, not rendered, so direct assignment is fine):
|
|
```dart
|
|
@override
|
|
void didChangeAppLifecycleState(AppLifecycleState state) {
|
|
super.didChangeAppLifecycleState(state);
|
|
switch (state) {
|
|
case AppLifecycleState.paused:
|
|
if (!_isInBackground) {
|
|
_isInBackground = true;
|
|
_handleBackgroundTransition();
|
|
}
|
|
break;
|
|
case AppLifecycleState.resumed:
|
|
_isInBackground = false;
|
|
break;
|
|
case AppLifecycleState.inactive:
|
|
case AppLifecycleState.detached:
|
|
case AppLifecycleState.hidden:
|
|
break;
|
|
}
|
|
}
|
|
```
|
|
|
|
**_handleBackgroundTransition changes:**
|
|
```dart
|
|
void _handleBackgroundTransition() {
|
|
_enableWakelock();
|
|
_requestBatteryOptimizationExemption();
|
|
if (context.read<OverlayProvider>().isOverlayActive) {
|
|
context.read<OverlayProvider>().pushCurrentData();
|
|
}
|
|
}
|
|
```
|
|
|
|
**build() method — the key migration:**
|
|
Use nested Selector/Consumer widgets for granular rebuilds. Structure:
|
|
|
|
```dart
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Selector<SettingsProvider, ColorTheme>(
|
|
selector: (_, settings) => settings.currentTheme,
|
|
builder: (context, currentTheme, _) {
|
|
return Scaffold(
|
|
backgroundColor: currentTheme.background,
|
|
body: SafeArea(
|
|
child: LayoutBuilder(
|
|
builder: (context, constraints) {
|
|
final isLandscape = constraints.maxWidth > constraints.maxHeight;
|
|
if (isLandscape) {
|
|
return _buildLandscapeLayout(currentTheme);
|
|
} else {
|
|
return _buildPortraitLayout(currentTheme);
|
|
}
|
|
},
|
|
),
|
|
),
|
|
);
|
|
},
|
|
);
|
|
}
|
|
```
|
|
|
|
**_buildPortraitLayout and _buildLandscapeLayout changes:**
|
|
Remove `speedText` parameter — speed is now obtained via Selector inside the layout.
|
|
|
|
The layout methods receive only `currentTheme` and use Selector widgets internally:
|
|
|
|
For the speed display area:
|
|
```dart
|
|
Selector<GpsDataManager, (double speed, String displaySpeed)>(
|
|
selector: (_, gps) => (gps.currentData.speed, gps.currentData.displaySpeed),
|
|
builder: (context, data, _) {
|
|
final settings = context.read<SettingsProvider>();
|
|
final displaySpeed = settings.currentUnit.convert(data.$1);
|
|
final speedText = _getSpeedDisplayText(displaySpeed);
|
|
return GestureDetector(
|
|
onTap: () => context.read<SettingsProvider>().cycleTheme(),
|
|
child: FittedBox(
|
|
fit: BoxFit.contain,
|
|
child: _buildSpeedDisplay(speedText, currentTheme, 200),
|
|
),
|
|
);
|
|
},
|
|
)
|
|
```
|
|
|
|
For the unit label:
|
|
```dart
|
|
Selector<SettingsProvider, String>(
|
|
selector: (_, settings) => settings.currentUnit.label,
|
|
builder: (context, unitLabel, _) {
|
|
return GestureDetector(
|
|
onTap: () => context.read<SettingsProvider>().cycleUnit(),
|
|
child: FittedBox(
|
|
fit: BoxFit.contain,
|
|
child: Text(
|
|
unitLabel,
|
|
style: TextStyle(
|
|
fontSize: 80,
|
|
color: currentTheme.unitText,
|
|
fontFamily: 'DIN1451Alt',
|
|
),
|
|
),
|
|
),
|
|
);
|
|
},
|
|
)
|
|
```
|
|
|
|
For the compass/navigation area heading display:
|
|
```dart
|
|
Selector<GpsDataManager, (double heading, String displayHeading)>(
|
|
selector: (_, gps) => (gps.currentData.heading, gps.currentData.displayHeading),
|
|
builder: (context, data, _) {
|
|
return Column(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
Flexible(
|
|
flex: 3,
|
|
child: Transform.rotate(
|
|
angle: (data.$1 >= 0 && data.$1 < 360) ? (data.$1 * math.pi / 180.0) : 0,
|
|
child: FittedBox(
|
|
fit: BoxFit.contain,
|
|
child: Icon(Icons.navigation, size: 80, color: currentTheme.headingText),
|
|
),
|
|
),
|
|
),
|
|
Flexible(
|
|
flex: 1,
|
|
child: FittedBox(
|
|
fit: BoxFit.contain,
|
|
child: Text(
|
|
data.$2,
|
|
style: TextStyle(fontSize: 50, color: currentTheme.headingText, fontFamily: 'DIN1451Alt'),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
);
|
|
},
|
|
)
|
|
```
|
|
|
|
For the compass area GestureDetector (overlay toggle):
|
|
```dart
|
|
GestureDetector(
|
|
onTap: () {
|
|
HapticFeedback.lightImpact();
|
|
context.read<OverlayProvider>().toggleOverlay();
|
|
},
|
|
onLongPress: () {
|
|
context.read<OverlayProvider>().closeOverlay();
|
|
},
|
|
child: Container(
|
|
// ... heading Selector inside
|
|
),
|
|
)
|
|
```
|
|
|
|
For the error message display, keep using the local `_errorMessage` state variable with a simple conditional check (no Provider needed — it's local).
|
|
|
|
**Critical performance pattern:** The speed Selector depends on BOTH GpsDataManager (for raw speed) AND SettingsProvider (for unit conversion). Use `context.read<SettingsProvider>()` inside the Selector builder (non-rebuilding read) since the speed Selector already rebuilds when GPS data changes, and the unit Selector handles unit label separately.
|
|
|
|
**Import changes:**
|
|
Add: `import 'package:provider/provider.dart';`
|
|
Add: `import '../providers/settings_provider.dart';`
|
|
Add: `import '../providers/overlay_provider.dart';`
|
|
Remove: `import '../config/color_themes.dart';` (accessed via SettingsProvider.currentTheme)
|
|
Remove: `import '../config/overlay_constants.dart';` (overlay sizing moved to OverlayProvider)
|
|
Remove: `import '../models/overlay_message.dart';` (overlay messaging moved to OverlayProvider)
|
|
Remove: `import 'package:flutter_overlay_window/flutter_overlay_window.dart';` (overlay interaction moved to OverlayProvider)
|
|
Remove: `import 'package:bg_launcher/bg_launcher.dart';` (bring-to-front moved to OverlayProvider)
|
|
Keep: `import '../config/speed_unit.dart';` (still needed for type reference in Selector)
|
|
Keep: `import '../config/timing_constants.dart';` (for heartbeat timer)
|
|
Keep: `import '../models/processed_gps_data.dart';` (for type in Selector)
|
|
Keep: `import '../services/gps_data_manager.dart';` (for Provider type access)
|
|
Keep: `import '../services/logger.dart';`
|
|
Keep: `import 'dart:async';` (for Timer)
|
|
Keep: `import 'dart:math' as math;` (for heading rotation)
|
|
Keep: `import 'package:wakelock_plus/wakelock_plus.dart';`
|
|
Keep: `import 'package:flutter/services.dart';` (for HapticFeedback in compass area)
|
|
</action>
|
|
<verify>
|
|
Run `flutter analyze` — no errors (SCREAMING_SNAKE warnings expected).
|
|
Search for `setState` in speedometer_screen.dart — should appear ONLY for `_errorMessage` (1 occurrence).
|
|
Search for `GpsDataManager.instance` — should appear ZERO times in the entire codebase.
|
|
Verify Selector<GpsDataManager, ...> exists for speed display.
|
|
Verify Selector<SettingsProvider, ...> exists for unit label.
|
|
Verify Selector<GpsDataManager, ...> exists for heading display.
|
|
Verify context.read<SettingsProvider>() used for cycleTheme/cycleUnit.
|
|
Verify context.read<OverlayProvider>() used for toggleOverlay/closeOverlay.
|
|
Verify no `_currentUnit`, `_currentThemeIndex`, `_isOverlayActive`, `_currentGpsData` state variables exist.
|
|
Verify dispose() only cancels _backgroundHeartbeatTimer and disables WakelockPlus.
|
|
</verify>
|
|
<done>
|
|
SpeedometerScreen fully migrated to Provider pattern. All shared state accessed via Consumer/Selector. Only local state (_errorMessage, _isInBackground, _backgroundHeartbeatTimer) uses setState or direct assignment. Speed display rebuilds independently of theme changes. Theme controls rebuild independently of GPS updates.
|
|
</done>
|
|
</task>
|
|
|
|
</tasks>
|
|
|
|
<verification>
|
|
After both tasks complete:
|
|
1. `flutter analyze` shows no errors
|
|
2. `flutter build apk --debug` compiles successfully
|
|
3. No `GpsDataManager.instance` anywhere in codebase (grep confirms)
|
|
4. No `setState` for shared state in speedometer_screen.dart (only _errorMessage)
|
|
5. MultiProvider correctly wired in main.dart with 4 providers
|
|
6. SpeedometerScreen uses Selector for speed display (does not rebuild on theme change)
|
|
7. SpeedometerScreen uses Selector for heading display (does not rebuild on unit change)
|
|
8. Overlay toggle works via OverlayProvider.toggleOverlay()
|
|
9. Background heartbeat still pushes data when overlay active
|
|
10. All providers properly disposed when app shuts down (via ChangeNotifierProvider lifecycle)
|
|
</verification>
|
|
|
|
<success_criteria>
|
|
- App compiles and runs with visually identical behavior to pre-migration
|
|
- Zero setState calls for shared state (theme, unit, GPS data, overlay status)
|
|
- Speed display rebuilds ONLY when GPS speed changes (not on theme/unit changes)
|
|
- Theme/unit cycling persists across app restarts via SharedPreferences
|
|
- Overlay show/close managed entirely by OverlayProvider
|
|
- Background heartbeat continues functioning for overlay data freshness
|
|
- No "setState after dispose" or "ChangeNotifier after dispose" errors
|
|
</success_criteria>
|
|
|
|
<output>
|
|
After completion, create `.planning/phases/02-provider-migration/02-02-SUMMARY.md`
|
|
</output>
|