docs(02): create phase plan
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
This commit is contained in:
@@ -0,0 +1,567 @@
|
||||
---
|
||||
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>
|
||||
Reference in New Issue
Block a user