docs: complete project research
Files: - STACK.md (recommended Provider + existing packages, no new dependencies) - FEATURES.md (restructure patterns, MVVM architecture requirements) - ARCHITECTURE.md (layer separation, overlay communication, data flow) - PITFALLS.md (async lifecycle issues, GPS leaks, battery drain) - SUMMARY.md (synthesis with 4-phase roadmap recommendations) Key findings: - Stack: Provider 6.1.x + existing geolocator/overlay packages (no additions needed) - Architecture: MVVM with layer-first structure, isolate-aware overlay messaging - Critical pitfall: setState after dispose during Provider migration (audit async patterns first) Overall confidence: HIGH Estimated effort: 52-70 hours Highest risk: Phase 2 (Provider migration) and Phase 3 (Overlay refactor) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,640 @@
|
||||
# Feature Landscape: Flutter GPS Speedometer Restructure
|
||||
|
||||
**Domain:** Flutter GPS/Speedometer App Architecture
|
||||
**Researched:** 2026-02-09
|
||||
**Confidence:** HIGH (verified with official Flutter docs + Android developer guides)
|
||||
|
||||
## Executive Summary
|
||||
|
||||
This research focuses on **HOW** features should be structured in a well-architected Flutter GPS speedometer app, not what new features to add. The current app suffers from monolithic architecture (1,133-line main.dart with raw setState), buggy overlay data synchronization, and aggressive power usage. Research reveals that modern 2026 Flutter architecture emphasizes MVVM/feature-first organization, proper state management with Provider (or migration to Riverpod), and adaptive GPS precision strategies.
|
||||
|
||||
---
|
||||
|
||||
## Table Stakes
|
||||
|
||||
Features/patterns users expect from a well-structured GPS speedometer app. Missing = restructure is pointless.
|
||||
|
||||
| Pattern | Why Expected | Complexity | Notes |
|
||||
|---------|--------------|------------|-------|
|
||||
| **MVVM Layer Separation** | Standard 2026 Flutter architecture - UI/Logic/Data layers must be distinct | Medium | View ↔ ViewModel ↔ Repository ↔ Service. Official Flutter recommendation. Prevents spaghetti code. |
|
||||
| **Feature-First Folder Structure** | Scales better than layer-first for multi-feature apps | Low | `lib/features/speedometer/`, `lib/features/overlay/`, `lib/core/`. Each feature self-contained. |
|
||||
| **Repository Pattern** | Single source of truth for GPS data - prevents state drift | Medium | GpsDataManager → Repository → Service pattern. Already partially implemented. |
|
||||
| **Provider Integration** | Move from raw setState to proper reactive state management | Medium | Consumer widgets + ChangeNotifier. Current app has Provider in dependencies but doesn't fully use it. |
|
||||
| **Speed-Adaptive GPS Precision** | Critical for battery life - high accuracy only when moving fast | High | Switch PRIORITY_HIGH_ACCURACY ↔ PRIORITY_BALANCED_POWER_ACCURACY based on speed thresholds (e.g., <5 km/h = balanced, >5 km/h = high). |
|
||||
| **Isolate-Aware Overlay Communication** | Overlay runs in separate isolate - needs proper message protocol | High | Use `FlutterOverlayWindow.shareData()` with structured messages. Current stale data issue stems from one-time sync instead of continuous updates. |
|
||||
| **Unidirectional Data Flow** | State flows downward: Data Layer → Logic Layer → UI Layer | Medium | Prevents circular dependencies. Events flow upward via commands/callbacks. |
|
||||
| **Service Layer Isolation** | GPS hardware access isolated to service layer only | Low | Already have `gps_service.dart`. Must ensure ViewModels never call Geolocator directly. |
|
||||
| **Lifecycle-Aware State Management** | Properly handle app backgrounding, overlay launch/close, GPS stream lifecycle | Medium | Use WidgetsBindingObserver + StreamSubscription cleanup. Partially implemented but needs formalization. |
|
||||
|
||||
---
|
||||
|
||||
## Differentiators
|
||||
|
||||
Patterns that make the architecture notably better than standard implementations.
|
||||
|
||||
| Pattern | Value Proposition | Complexity | Notes |
|
||||
|---------|-------------------|------------|-------|
|
||||
| **Hybrid GPS Strategy** | Better battery life than continuous HIGH_ACCURACY - adaptively switches precision based on movement context | High | Combine speed threshold + WiFi connection state + motion sensors. Example: stationary + WiFi connected = PASSIVE mode, slow movement = BALANCED, fast movement = HIGH_ACCURACY. |
|
||||
| **Message Queue for Overlay Sync** | Prevents data staleness - overlay receives continuous updates via message stream | Medium | Implement periodic broadcast (e.g., every 500ms) with debouncing. Queue messages if overlay isn't ready. Better than current one-shot messaging. |
|
||||
| **WorkManager Background Tasks** | Modern alternative to wake locks - system handles power management | Medium | Use WorkManager for periodic location checks when app backgrounded. Replaces current aggressive wake lock + heartbeat timer pattern. Reduces Google Play Store policy violations. |
|
||||
| **ViewModel Command Pattern** | Cleaner event handling - views call commands, ViewModels never expose widgets | Low | `viewModel.onThemeToggle()` instead of callbacks. Makes testing easier and separates concerns. |
|
||||
| **Multi-Repository ViewModel** | Speedometer feature needs GPS + Settings + Overlay state - composing multiple repositories prevents monolithic ViewModels | Medium | SpeedometerViewModel depends on GpsRepository + SettingsRepository + OverlayRepository. Each repository focused on single domain. |
|
||||
| **Duty-Cycle Awareness** | Android 9+ duty-cycles GNSS hardware - app should detect and adapt update frequency expectations | Medium | Monitor GPS timestamp deltas. If duty-cycling detected (irregular intervals), adjust UI update strategy to interpolate between readings. |
|
||||
| **Scoped Provider Instances** | Overlay and main app have separate Provider scopes - prevents state collision | Medium | Each isolate (main app vs overlay) has its own Provider tree. Use message passing for synchronization, not shared state. Critical for fixing current stale data bug. |
|
||||
| **Foreground Service Architecture** | Proper Android foreground service with notification for GPS tracking - prevents system kill | Low | Already have SYSTEM_ALERT_WINDOW permission. Add `android:foregroundServiceType="location"` and show persistent notification while tracking active. |
|
||||
|
||||
---
|
||||
|
||||
## Anti-Features
|
||||
|
||||
Things to deliberately NOT do during restructuring. Common mistakes in this domain.
|
||||
|
||||
| Anti-Pattern | Why Avoid | What to Do Instead |
|
||||
|--------------|-----------|-------------------|
|
||||
| **Monolithic ViewModel/Widget** | Current 1,133-line main.dart is technical debt. Difficult to test, reason about, or modify. | Split into feature-specific ViewModels: `SpeedometerViewModel`, `OverlayViewModel`, `SettingsViewModel`. Each <200 lines. |
|
||||
| **Shared State Between Isolates** | Flutter overlay runs in separate isolate - cannot share memory. Current stale data bug stems from assuming shared state. | Use message passing only. Send serialized GPS data via `shareData()`. No global singletons accessible from both isolates. |
|
||||
| **Continuous HIGH_ACCURACY GPS** | Current app uses HIGH_ACCURACY + wake locks continuously. Drains 7-12% battery per hour. Violates 2026 Google Play Store excessive wake lock policy. | Implement speed-adaptive precision. Switch to BALANCED_POWER_ACCURACY when stationary or slow (<5 km/h). Use PASSIVE mode when WiFi connected + stationary. |
|
||||
| **Wake Lock Without Justification** | Current permanent wake lock prevents system power management. Google penalizes apps exceeding 2 hours wake lock in 24 hours. | Replace with WorkManager for background operation. Only use wake lock during active navigation/overlay display. Release wake lock when app backgrounded without overlay. |
|
||||
| **setState for Cross-Widget State** | Using setState in main.dart forces entire screen rebuilds. Doesn't scale to overlay sync. | Migrate to Provider pattern fully. Use `Consumer` widgets at leaf nodes. Only rebuild widgets that depend on changed state. |
|
||||
| **Synchronous Overlay Data Push** | Current "push once on overlay launch" approach causes stale data. Overlay has no way to request updates. | Implement continuous broadcast stream. Overlay subscribes to message stream and updates reactively. Debounce to 2-4 updates/second max. |
|
||||
| **Ignoring Duty-Cycling** | Android duty-cycles GPS after Android 9 to save battery. Expecting continuous updates leads to UI jank when updates arrive in bursts. | Buffer GPS readings and interpolate between updates for smooth UI. Accept that <1s update frequency may not be achievable on battery-optimized devices. |
|
||||
| **Repository Coupling** | Having GpsDataManager directly call overlay messaging violates separation of concerns. | GpsRepository emits data streams. Separate OverlayService subscribes to GPS stream and handles messaging. Repositories unaware of each other. |
|
||||
| **Missing Error State Handling** | Current error messages displayed directly in UI. No way to recover from GPS permission denial or service disabled. | ViewModels expose error state objects. Views render error UI with action buttons (e.g., "Enable GPS" button opens system settings via SettingsClient). |
|
||||
| **Over-Engineering with BLoC** | For this app's complexity, BLoC would add boilerplate without benefit. Current team already using Provider. | Stick with Provider + MVVM. Riverpod migration optional but not required. BLoC overkill for single-developer GPS app. |
|
||||
|
||||
---
|
||||
|
||||
## Feature Dependencies
|
||||
|
||||
Patterns have ordering dependencies - some must be implemented before others.
|
||||
|
||||
```
|
||||
Foundation Layer (implement first):
|
||||
├── Feature-First Folder Structure
|
||||
├── Service Layer Isolation (GPS, Settings, Overlay)
|
||||
└── Repository Pattern (single source of truth)
|
||||
↓
|
||||
State Management Layer (depends on repositories):
|
||||
├── Provider Integration (ChangeNotifier ViewModels)
|
||||
├── Unidirectional Data Flow
|
||||
└── Lifecycle-Aware State Management
|
||||
↓
|
||||
GPS Optimization Layer (depends on state management):
|
||||
├── Speed-Adaptive GPS Precision
|
||||
├── Duty-Cycle Awareness
|
||||
└── WorkManager Background Tasks (replaces wake locks)
|
||||
↓
|
||||
Overlay Communication Layer (depends on all above):
|
||||
├── Isolate-Aware Messaging Protocol
|
||||
├── Message Queue for Continuous Sync
|
||||
└── Scoped Provider Instances (separate isolate contexts)
|
||||
```
|
||||
|
||||
**Critical path**: Cannot fix overlay stale data issue until Provider properly integrated + message queue implemented + scoped instances established.
|
||||
|
||||
---
|
||||
|
||||
## MVP Recommendation
|
||||
|
||||
For restructure MVP, prioritize these patterns (in order):
|
||||
|
||||
### Phase 1: Foundation (Must Have)
|
||||
1. **Feature-First Folder Structure** - Extract features from monolithic main.dart
|
||||
2. **MVVM Layer Separation** - Create View/ViewModel/Repository/Service layers
|
||||
3. **Provider Integration** - Replace setState with ChangeNotifier + Consumer
|
||||
|
||||
**Rationale**: These establish architectural foundation. Without these, remaining patterns cannot be properly implemented.
|
||||
|
||||
### Phase 2: GPS Optimization (Should Have)
|
||||
4. **Speed-Adaptive GPS Precision** - Implement BALANCED ↔ HIGH_ACCURACY switching
|
||||
5. **Repository Pattern** - Formalize GpsDataManager as proper Repository
|
||||
6. **WorkManager Background Tasks** - Replace aggressive wake lock usage
|
||||
|
||||
**Rationale**: Addresses current battery drain and Google Play Store policy compliance. Can be implemented once foundation exists.
|
||||
|
||||
### Phase 3: Overlay Fix (Must Have for Feature Parity)
|
||||
7. **Isolate-Aware Overlay Communication** - Implement proper message protocol
|
||||
8. **Message Queue for Continuous Sync** - Fix stale data bug with stream-based updates
|
||||
9. **Scoped Provider Instances** - Separate state management per isolate
|
||||
|
||||
**Rationale**: Fixes critical overlay bug. Blocked by Phase 1 Provider integration.
|
||||
|
||||
### Defer to Post-Restructure:
|
||||
|
||||
- **Duty-Cycle Awareness**: Nice-to-have optimization, not critical for functionality
|
||||
- **Multi-Repository ViewModel**: Current app simple enough for single GPS repository
|
||||
- **Hybrid GPS Strategy**: Advanced optimization - basic speed-adaptive sufficient for MVP
|
||||
- **Foreground Service Architecture**: Already working, no immediate need to refactor
|
||||
|
||||
---
|
||||
|
||||
## Architecture Pattern Examples
|
||||
|
||||
### Current Problematic Pattern
|
||||
|
||||
```dart
|
||||
// main.dart - 1,133 lines, monolithic
|
||||
class _SpeedometerScreenState extends State<SpeedometerScreen> {
|
||||
SpeedUnit _currentUnit = SpeedUnit.kmh; // Settings state
|
||||
int _currentThemeIndex = 0; // Theme state
|
||||
ProcessedGpsData _currentGpsData = ...; // GPS state
|
||||
bool _isOverlayActive = false; // Overlay state
|
||||
Timer? _backgroundHeartbeatTimer; // Background state
|
||||
|
||||
// All business logic mixed with UI logic
|
||||
void _startGPS() { ... }
|
||||
void _updateOverlay() { ... }
|
||||
Widget build(BuildContext context) { /* 800+ lines */ }
|
||||
}
|
||||
```
|
||||
|
||||
**Problems:**
|
||||
- Single file responsible for 5+ concerns
|
||||
- No testability (widget tests required for business logic)
|
||||
- setState forces entire screen rebuild
|
||||
- Overlay state coupled to main screen state
|
||||
|
||||
### Recommended Pattern: Feature-First MVVM
|
||||
|
||||
```
|
||||
lib/
|
||||
├── features/
|
||||
│ ├── speedometer/
|
||||
│ │ ├── presentation/
|
||||
│ │ │ ├── speedometer_screen.dart // View (UI only)
|
||||
│ │ │ └── widgets/
|
||||
│ │ │ ├── digital_display.dart
|
||||
│ │ │ └── analog_gauge.dart
|
||||
│ │ ├── viewmodels/
|
||||
│ │ │ └── speedometer_viewmodel.dart // ViewModel (UI logic)
|
||||
│ │ └── data/
|
||||
│ │ └── repositories/
|
||||
│ │ └── gps_repository.dart // Repository (data logic)
|
||||
│ │
|
||||
│ ├── overlay/
|
||||
│ │ ├── presentation/
|
||||
│ │ │ └── overlay_screen.dart // Separate isolate UI
|
||||
│ │ ├── viewmodels/
|
||||
│ │ │ └── overlay_viewmodel.dart // Overlay-specific logic
|
||||
│ │ └── services/
|
||||
│ │ └── overlay_messaging_service.dart // Message protocol
|
||||
│ │
|
||||
│ └── settings/
|
||||
│ ├── presentation/
|
||||
│ │ └── settings_screen.dart
|
||||
│ ├── viewmodels/
|
||||
│ │ └── settings_viewmodel.dart
|
||||
│ └── data/
|
||||
│ └── repositories/
|
||||
│ └── settings_repository.dart // SharedPreferences wrapper
|
||||
│
|
||||
├── core/
|
||||
│ ├── services/
|
||||
│ │ ├── gps_service.dart // Hardware abstraction
|
||||
│ │ └── permission_service.dart
|
||||
│ └── models/
|
||||
│ ├── processed_gps_data.dart
|
||||
│ └── speed_unit.dart
|
||||
│
|
||||
└── main.dart // App entry point only
|
||||
```
|
||||
|
||||
**Benefits:**
|
||||
- Each feature self-contained
|
||||
- Business logic testable without Flutter
|
||||
- Clear responsibility boundaries
|
||||
- Easy to locate code by feature
|
||||
|
||||
### Recommended Pattern: ViewModel with Provider
|
||||
|
||||
```dart
|
||||
// speedometer_viewmodel.dart
|
||||
class SpeedometerViewModel extends ChangeNotifier {
|
||||
final GpsRepository _gpsRepository;
|
||||
final SettingsRepository _settingsRepository;
|
||||
|
||||
// UI State (derived from repositories)
|
||||
ProcessedGpsData? _currentGpsData;
|
||||
bool _isLoading = true;
|
||||
String? _errorMessage;
|
||||
|
||||
SpeedometerViewModel({
|
||||
required GpsRepository gpsRepository,
|
||||
required SettingsRepository settingsRepository,
|
||||
}) : _gpsRepository = gpsRepository,
|
||||
_settingsRepository = settingsRepository {
|
||||
_initialize();
|
||||
}
|
||||
|
||||
// Getters (read-only state for views)
|
||||
ProcessedGpsData? get gpsData => _currentGpsData;
|
||||
bool get isLoading => _isLoading;
|
||||
String? get errorMessage => _errorMessage;
|
||||
SpeedUnit get currentUnit => _settingsRepository.speedUnit;
|
||||
|
||||
// Commands (view actions)
|
||||
Future<void> refreshGPS() async {
|
||||
// Business logic here
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void toggleSpeedUnit() {
|
||||
_settingsRepository.toggleSpeedUnit();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void _initialize() {
|
||||
// Subscribe to GPS stream
|
||||
_gpsRepository.gpsStream.listen(
|
||||
(data) {
|
||||
_currentGpsData = data;
|
||||
_isLoading = false;
|
||||
_errorMessage = null;
|
||||
notifyListeners();
|
||||
},
|
||||
onError: (error) {
|
||||
_errorMessage = error.toString();
|
||||
_isLoading = false;
|
||||
notifyListeners();
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
// Cleanup subscriptions
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Benefits:**
|
||||
- Testable without widgets (mock repositories)
|
||||
- Clear command pattern (toggleSpeedUnit vs callbacks)
|
||||
- State changes trigger only necessary rebuilds
|
||||
- Repository dependencies injected (testable, swappable)
|
||||
|
||||
### Recommended Pattern: Consumer Widget Usage
|
||||
|
||||
```dart
|
||||
// speedometer_screen.dart (View only)
|
||||
class SpeedometerScreen extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
body: Column(
|
||||
children: [
|
||||
// Only rebuild speed display when GPS data changes
|
||||
Consumer<SpeedometerViewModel>(
|
||||
builder: (context, viewModel, child) {
|
||||
if (viewModel.isLoading) {
|
||||
return CircularProgressIndicator();
|
||||
}
|
||||
|
||||
if (viewModel.errorMessage != null) {
|
||||
return ErrorWidget(
|
||||
message: viewModel.errorMessage!,
|
||||
onRetry: viewModel.refreshGPS,
|
||||
);
|
||||
}
|
||||
|
||||
return DigitalSpeedDisplay(
|
||||
speed: viewModel.gpsData?.displaySpeed ?? '--',
|
||||
unit: viewModel.currentUnit,
|
||||
);
|
||||
},
|
||||
),
|
||||
|
||||
// Static child (doesn't rebuild on every GPS update)
|
||||
Consumer<SpeedometerViewModel>(
|
||||
builder: (context, viewModel, child) {
|
||||
return SpeedUnitToggle(
|
||||
currentUnit: viewModel.currentUnit,
|
||||
onToggle: viewModel.toggleSpeedUnit,
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Benefits:**
|
||||
- View is "dumb" - no business logic
|
||||
- Consumer scoped to minimize rebuilds
|
||||
- Clear separation: view renders, viewModel decides what to render
|
||||
- Testable: mock ViewModel, verify UI rendering
|
||||
|
||||
### Recommended Pattern: Speed-Adaptive GPS Precision
|
||||
|
||||
```dart
|
||||
// gps_repository.dart
|
||||
class GpsRepository {
|
||||
static const double STATIONARY_THRESHOLD_MPS = 1.4; // ~5 km/h
|
||||
static const double MOVING_THRESHOLD_MPS = 8.3; // ~30 km/h
|
||||
|
||||
LocationAccuracy _currentAccuracy = LocationAccuracy.balanced;
|
||||
double _lastSpeed = 0.0;
|
||||
|
||||
void _updateAccuracyBasedOnSpeed(double speedMps) {
|
||||
final previousAccuracy = _currentAccuracy;
|
||||
|
||||
// State machine for accuracy switching
|
||||
if (speedMps < STATIONARY_THRESHOLD_MPS) {
|
||||
_currentAccuracy = LocationAccuracy.low; // PRIORITY_BALANCED_POWER_ACCURACY
|
||||
} else if (speedMps > MOVING_THRESHOLD_MPS) {
|
||||
_currentAccuracy = LocationAccuracy.best; // PRIORITY_HIGH_ACCURACY
|
||||
}
|
||||
// Hysteresis: stay in current mode if between thresholds
|
||||
|
||||
// Only reconfigure GPS if accuracy changed
|
||||
if (previousAccuracy != _currentAccuracy) {
|
||||
_reconfigureGpsStream(_currentAccuracy);
|
||||
debugPrint('GPS accuracy changed: $previousAccuracy → $_currentAccuracy');
|
||||
}
|
||||
|
||||
_lastSpeed = speedMps;
|
||||
}
|
||||
|
||||
void _reconfigureGpsStream(LocationAccuracy accuracy) {
|
||||
// Cancel existing stream
|
||||
_gpsSubscription?.cancel();
|
||||
|
||||
// Create new stream with updated accuracy
|
||||
final locationSettings = LocationSettings(
|
||||
accuracy: accuracy,
|
||||
distanceFilter: accuracy == LocationAccuracy.best ? 5 : 50,
|
||||
);
|
||||
|
||||
_gpsSubscription = Geolocator.getPositionStream(
|
||||
locationSettings: locationSettings,
|
||||
).listen(_handleGpsUpdate);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Benefits:**
|
||||
- Automatic switching based on movement
|
||||
- Hysteresis prevents rapid toggling
|
||||
- Clear thresholds (can be tuned)
|
||||
- Logs accuracy changes for debugging
|
||||
|
||||
### Recommended Pattern: Overlay Message Protocol
|
||||
|
||||
```dart
|
||||
// overlay_messaging_service.dart
|
||||
class OverlayMessagingService {
|
||||
static const Duration UPDATE_INTERVAL = Duration(milliseconds: 500);
|
||||
Timer? _broadcastTimer;
|
||||
|
||||
void startBroadcasting(Stream<ProcessedGpsData> gpsStream) {
|
||||
// Debounced broadcast to prevent message flooding
|
||||
_broadcastTimer = Timer.periodic(UPDATE_INTERVAL, (_) async {
|
||||
final latestData = _latestGpsData;
|
||||
if (latestData != null) {
|
||||
await _sendToOverlay(latestData);
|
||||
}
|
||||
});
|
||||
|
||||
// Subscribe to GPS stream
|
||||
gpsStream.listen((data) {
|
||||
_latestGpsData = data;
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _sendToOverlay(ProcessedGpsData data) async {
|
||||
// Structured message format
|
||||
final message = jsonEncode({
|
||||
'type': 'gps_update',
|
||||
'timestamp': DateTime.now().millisecondsSinceEpoch,
|
||||
'data': {
|
||||
'displaySpeed': data.displaySpeed,
|
||||
'displayHeading': data.displayHeading,
|
||||
'isSpeedValid': data.isSpeedValid,
|
||||
'isHeadingValid': data.isHeadingValid,
|
||||
},
|
||||
});
|
||||
|
||||
try {
|
||||
await FlutterOverlayWindow.shareData(message);
|
||||
} catch (e) {
|
||||
debugPrint('Failed to send overlay message: $e');
|
||||
// Don't crash main app if overlay messaging fails
|
||||
}
|
||||
}
|
||||
|
||||
void stopBroadcasting() {
|
||||
_broadcastTimer?.cancel();
|
||||
_broadcastTimer = null;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Benefits:**
|
||||
- Continuous updates (fixes stale data bug)
|
||||
- Debounced to prevent message flooding (500ms = 2 updates/second)
|
||||
- Structured JSON messages (extensible)
|
||||
- Graceful failure handling (overlay may not be active)
|
||||
- Timestamp allows overlay to detect stale messages
|
||||
|
||||
### Recommended Pattern: Overlay Scoped Provider
|
||||
|
||||
```dart
|
||||
// overlay_main.dart (separate entry point)
|
||||
@pragma("vm:entry-point")
|
||||
void overlayMain() {
|
||||
runApp(
|
||||
MultiProvider(
|
||||
providers: [
|
||||
ChangeNotifierProvider(
|
||||
create: (_) => OverlayViewModel(), // Separate instance from main app
|
||||
),
|
||||
],
|
||||
child: MaterialApp(
|
||||
home: OverlayScreen(),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// overlay_viewmodel.dart
|
||||
class OverlayViewModel extends ChangeNotifier {
|
||||
ProcessedGpsData? _currentGpsData;
|
||||
DateTime? _lastUpdate;
|
||||
|
||||
OverlayViewModel() {
|
||||
_startListening();
|
||||
}
|
||||
|
||||
void _startListening() {
|
||||
// Listen for messages from main app
|
||||
FlutterOverlayWindow.overlayListener.listen((rawMessage) {
|
||||
try {
|
||||
final message = jsonDecode(rawMessage);
|
||||
|
||||
if (message['type'] == 'gps_update') {
|
||||
_currentGpsData = ProcessedGpsData.fromJson(message['data']);
|
||||
_lastUpdate = DateTime.fromMillisecondsSinceEpoch(
|
||||
message['timestamp'],
|
||||
);
|
||||
notifyListeners();
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('Failed to parse overlay message: $e');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Check if data is stale (no updates for 2+ seconds)
|
||||
bool get isDataStale {
|
||||
if (_lastUpdate == null) return true;
|
||||
return DateTime.now().difference(_lastUpdate!) > Duration(seconds: 2);
|
||||
}
|
||||
|
||||
ProcessedGpsData? get gpsData => isDataStale ? null : _currentGpsData;
|
||||
}
|
||||
```
|
||||
|
||||
**Benefits:**
|
||||
- Overlay has its own Provider scope (separate isolate)
|
||||
- Reactive updates via overlayListener
|
||||
- Stale data detection (shows "--" if no recent updates)
|
||||
- No shared state between main app and overlay
|
||||
|
||||
---
|
||||
|
||||
## Complexity Assessment
|
||||
|
||||
| Pattern Category | Overall Complexity | Implementation Time | Risk |
|
||||
|------------------|-------------------|---------------------|------|
|
||||
| **MVVM Layer Separation** | Medium | 8-12 hours | Low - well-documented pattern |
|
||||
| **Feature-First Structure** | Low | 4-6 hours | Low - file reorganization |
|
||||
| **Provider Integration** | Medium | 6-8 hours | Medium - requires careful migration from setState |
|
||||
| **Speed-Adaptive GPS** | High | 12-16 hours | Medium - requires testing on real devices |
|
||||
| **Overlay Communication** | High | 16-20 hours | High - isolate debugging difficult |
|
||||
| **WorkManager Background** | Medium | 6-8 hours | Medium - Android platform-specific |
|
||||
|
||||
**Total restructure estimate**: 52-70 hours (1.5-2 weeks full-time)
|
||||
|
||||
**Highest risk areas**:
|
||||
1. Overlay communication - isolates cannot be debugged easily, requires extensive physical device testing
|
||||
2. Speed-adaptive GPS - thresholds need real-world tuning, different Android versions behave differently
|
||||
3. Provider migration - easy to miss setState calls, requires thorough testing
|
||||
|
||||
---
|
||||
|
||||
## Testing Strategy Implications
|
||||
|
||||
Well-structured architecture enables better testing:
|
||||
|
||||
### Unit Tests (Business Logic)
|
||||
```dart
|
||||
// Test ViewModel without Flutter framework
|
||||
test('SpeedometerViewModel toggles speed unit', () {
|
||||
final mockGpsRepo = MockGpsRepository();
|
||||
final mockSettingsRepo = MockSettingsRepository();
|
||||
|
||||
final viewModel = SpeedometerViewModel(
|
||||
gpsRepository: mockGpsRepo,
|
||||
settingsRepository: mockSettingsRepo,
|
||||
);
|
||||
|
||||
viewModel.toggleSpeedUnit();
|
||||
|
||||
verify(mockSettingsRepo.toggleSpeedUnit()).called(1);
|
||||
});
|
||||
```
|
||||
|
||||
### Widget Tests (UI)
|
||||
```dart
|
||||
// Test View rendering without business logic
|
||||
testWidgets('SpeedometerScreen displays speed from ViewModel', (tester) async {
|
||||
final mockViewModel = MockSpeedometerViewModel();
|
||||
when(mockViewModel.gpsData).thenReturn(
|
||||
ProcessedGpsData(displaySpeed: '120', ...),
|
||||
);
|
||||
|
||||
await tester.pumpWidget(
|
||||
ChangeNotifierProvider<SpeedometerViewModel>.value(
|
||||
value: mockViewModel,
|
||||
child: SpeedometerScreen(),
|
||||
),
|
||||
);
|
||||
|
||||
expect(find.text('120'), findsOneWidget);
|
||||
});
|
||||
```
|
||||
|
||||
### Integration Tests (End-to-End)
|
||||
```dart
|
||||
// Test GPS → ViewModel → View flow
|
||||
testWidgets('GPS updates trigger UI updates', (tester) async {
|
||||
final realGpsRepo = GpsRepository(gpsService: FakeGpsService());
|
||||
|
||||
await tester.pumpWidget(MyApp(gpsRepository: realGpsRepo));
|
||||
|
||||
// Simulate GPS update
|
||||
realGpsRepo.injectFakePosition(speed: 50.0);
|
||||
await tester.pump();
|
||||
|
||||
expect(find.text('50'), findsOneWidget);
|
||||
});
|
||||
```
|
||||
|
||||
**Current app**: Cannot unit test business logic (requires widget tests for everything). Cannot mock GPS (tightly coupled to hardware).
|
||||
|
||||
**Restructured app**: Business logic testable in isolation. Views testable with mocked ViewModels. Integration tests verify end-to-end flow.
|
||||
|
||||
---
|
||||
|
||||
## Conclusion
|
||||
|
||||
The restructure focuses on architectural patterns (HOW to structure), not new features (WHAT to build). Priority order:
|
||||
|
||||
1. **Foundation** (Feature-first + MVVM + Provider) - enables everything else
|
||||
2. **GPS Optimization** (Speed-adaptive + WorkManager) - addresses battery drain
|
||||
3. **Overlay Fix** (Message queue + Scoped providers) - resolves stale data bug
|
||||
|
||||
The current 1,133-line monolithic main.dart with raw setState and aggressive wake locks represents 2018-era Flutter development. The 2026 standard is MVVM with Provider/Riverpod, feature-first organization, adaptive GPS precision, and proper Android lifecycle management.
|
||||
|
||||
Key success metric: Main screen logic reduced from 1,133 lines to <200 lines per ViewModel. Business logic testable without Flutter framework. Overlay data stays fresh within 500ms of main app.
|
||||
|
||||
---
|
||||
|
||||
## Sources
|
||||
|
||||
### Flutter Architecture
|
||||
- [Guide to app architecture - Flutter](https://docs.flutter.dev/app-architecture/guide)
|
||||
- [Architecture design patterns - Flutter](https://docs.flutter.dev/app-architecture/design-patterns)
|
||||
- [Modern Flutter Architecture Patterns - Medium](https://medium.com/@sharmapraveen91/modern-flutter-architecture-patterns-ed6882a11b7c)
|
||||
- [Flutter Project Structure: Feature-first or Layer-first?](https://codewithandrea.com/articles/flutter-project-structure/)
|
||||
- [Flutter App Architecture: The Repository Pattern](https://codewithandrea.com/articles/flutter-repository-pattern/)
|
||||
|
||||
### State Management
|
||||
- [Best Flutter State Management Libraries 2026](https://foresightmobile.com/blog/best-flutter-state-management)
|
||||
- [The Ultimate Guide to Flutter State Management in 2026](https://medium.com/@satishparmarparmar486/the-ultimate-guide-to-flutter-state-management-in-2026-from-setstate-to-bloc-riverpod-561192c31e1c)
|
||||
- [State Management in Flutter: Provider vs Riverpod vs Bloc](https://ms3byoussef.medium.com/state-management-in-flutter-provider-vs-riverpod-vs-bloc-333795f0df22)
|
||||
- [Flutter ChangeNotifier for Simplified State Management](https://www.dhiwise.com/post/fluidstate-management-with-flutter-changenotifier)
|
||||
|
||||
### Android Overlay Windows
|
||||
- [flutter_overlay_window | Flutter package](https://pub.dev/packages/flutter_overlay_window)
|
||||
- [system_alert_window | Flutter package](https://pub.dev/packages/system_alert_window)
|
||||
- [Concurrency and isolates - Flutter](https://docs.flutter.dev/perf/isolates)
|
||||
|
||||
### GPS & Location Services
|
||||
- [Change location settings - Android Developers](https://developer.android.com/develop/sensors-and-location/location/change-location-settings)
|
||||
- [About background location and battery life - Android Developers](https://developer.android.com/develop/sensors-and-location/location/battery)
|
||||
- [How to Implement Geolocation Without Draining Battery](https://metova.com/how-to-implement-geolocation-without-draining-your-users-battery/)
|
||||
- [Improving urban GPS accuracy for your app - Android Developers Blog](https://android-developers.googleblog.com/2020/12/improving-urban-gps-accuracy-for-your.html)
|
||||
|
||||
### Background Work & Battery Optimization
|
||||
- [Optimize your app battery using Android vitals wake lock metric](https://android-developers.googleblog.com/2025/09/guide-to-excessive-wake-lock-usage.html)
|
||||
- [JobScheduler vs WorkManager - Medium](https://medium.com/@vaibhav.shakya786/jobscheduler-vs-workmanager-the-battle-for-androids-background-work-fcceec5cd6ff)
|
||||
- [Replace Android Foreground Services with WorkManager](https://medium.com/@manish_bannur/replace-android-foreground-services-with-workmanager-a739f7c8ff76)
|
||||
|
||||
### Flutter Best Practices 2026
|
||||
- [Best Practices to implement for Flutter App Development in 2026](https://www.manektech.com/blog/flutter-development-best-practices)
|
||||
- [Flutter App Development in 2026: 8 Best Practices](https://otfcoder.com/flutter-app-development-best-practices-for-scalable-apps/)
|
||||
- [Stop Doing These Flutter Performance Mistakes (2026 Edition)](https://medium.com/@tiger.chirag/stop-doing-these-flutter-performance-mistakes-2026-edition-79cae09d5f22)
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,708 @@
|
||||
# Technology Stack: Flutter GPS Speedometer Restructure
|
||||
|
||||
**Project:** GPS Speedometer App Modularization
|
||||
**Researched:** 2026-02-09
|
||||
**Milestone Context:** Restructuring existing monolithic app into modular architecture with Provider state management and Android GPS/overlay improvements
|
||||
|
||||
---
|
||||
|
||||
## Core Principle: No New Major Dependencies
|
||||
|
||||
This restructure focuses on leveraging existing dependencies properly rather than introducing new packages. The stack remains deliberately minimal.
|
||||
|
||||
---
|
||||
|
||||
## Recommended Stack
|
||||
|
||||
### Core Framework
|
||||
| Technology | Version | Purpose | Rationale |
|
||||
|------------|---------|---------|-----------|
|
||||
| Flutter SDK | 3.24+ | Cross-platform framework | Current stable, Material 3 support |
|
||||
| Dart SDK | 3.9+ | Programming language | Already in pubspec, async/null safety features |
|
||||
|
||||
**Confidence:** HIGH (Existing project versions)
|
||||
|
||||
### State Management
|
||||
| Technology | Version | Purpose | Rationale |
|
||||
|------------|---------|---------|-----------|
|
||||
| **Provider** | 6.1.x | UI state management | Already in dependencies but unused. Official Flutter recommendation for simple-to-moderate state. Mature, well-documented, low boilerplate. Adequate for GPS data streaming + UI preferences. |
|
||||
|
||||
**Why Provider over alternatives:**
|
||||
- **NOT Riverpod:** While Riverpod 3.0 is the 2026 community favorite and fixes Provider's BuildContext limitations, migration adds scope creep. Provider is sufficient for this app's complexity.
|
||||
- **NOT BLoC:** Overkill for a single-screen app with straightforward GPS data flow.
|
||||
- **NOT setState alone:** Current 1,075-line monolithic main.dart proves setState doesn't scale.
|
||||
|
||||
**Confidence:** HIGH (Based on [Flutter official docs](https://docs.flutter.dev/data-and-backend/state-mgmt/simple) and [2026 state management surveys](https://foresightmobile.com/blog/best-flutter-state-management))
|
||||
|
||||
### Location Services
|
||||
| Technology | Version | Purpose | Rationale |
|
||||
|------------|---------|---------|-----------|
|
||||
| geolocator | 14.0.2 | GPS position stream | Already integrated. Supports LocationSettings for adaptive precision control. Android 14+ compatible with FOREGROUND_SERVICE_LOCATION. |
|
||||
| permission_handler | 12.0.1 | Runtime permissions | Already integrated. Handles Android location permission flows. |
|
||||
|
||||
**Key API Pattern for Adaptive GPS Precision:**
|
||||
|
||||
```dart
|
||||
// Speed-adaptive GPS configuration
|
||||
LocationSettings _getAdaptiveLocationSettings(double currentSpeed) {
|
||||
// Use HIGH_ACCURACY above ~10 km/h, BALANCED below with hysteresis
|
||||
final bool useHighAccuracy = currentSpeed > 3.0; // ~10.8 km/h threshold
|
||||
|
||||
return AndroidSettings(
|
||||
accuracy: useHighAccuracy
|
||||
? LocationAccuracy.high // PRIORITY_HIGH_ACCURACY (0-100m)
|
||||
: LocationAccuracy.medium, // PRIORITY_BALANCED_POWER_ACCURACY (~100m)
|
||||
intervalDuration: Duration(seconds: 1),
|
||||
distanceFilter: useHighAccuracy ? 0 : 5, // Filter noise when slow
|
||||
foregroundNotificationConfig: ForegroundNotificationConfig(
|
||||
notificationText: "GPS Speedometer is tracking your location",
|
||||
notificationTitle: "GPS Active",
|
||||
enableWakeLock: true,
|
||||
),
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
**LocationAccuracy → Android Priority Mapping:**
|
||||
| Flutter Enum | Android Native | Accuracy | Power | Use Case |
|
||||
|--------------|----------------|----------|-------|----------|
|
||||
| `LocationAccuracy.bestForNavigation` | `PRIORITY_HIGH_ACCURACY` | 0-100m | HIGH | Real-time driving (your HIGH state) |
|
||||
| `LocationAccuracy.high` | `PRIORITY_HIGH_ACCURACY` | 0-100m | HIGH | Real-time driving (your HIGH state) |
|
||||
| `LocationAccuracy.medium` | `PRIORITY_BALANCED_POWER_ACCURACY` | ~100m | MEDIUM | Low-speed / stationary (your BALANCED state) |
|
||||
| `LocationAccuracy.low` | `PRIORITY_LOW_POWER` | 100-500m | LOW | Background tracking (not needed) |
|
||||
| `LocationAccuracy.lowest` | `PRIORITY_PASSIVE` | 500m+ | MINIMAL | Piggyback only (not needed) |
|
||||
|
||||
**Important:** Use `AndroidSettings.intervalDuration` to control update frequency. The deprecated `getCurrentPosition()` parameters (`desiredAccuracy`, `timeLimit`) were replaced with `LocationSettings` in geolocator 6.0+.
|
||||
|
||||
**Confidence:** HIGH (Official [geolocator docs](https://pub.dev/packages/geolocator) and [Android LocationRequest API](https://developer.android.com/develop/sensors-and-location/location/request-updates))
|
||||
|
||||
### Android Overlay Window
|
||||
| Technology | Version | Purpose | Rationale | Status |
|
||||
|------------|---------|---------|-----------|--------|
|
||||
| flutter_overlay_window | 0.5.0 | System-level overlay | Already integrated. **Known reliability issues:** bidirectional communication unstable, overlay positioning poor, performance impact on main app. | PROBLEMATIC |
|
||||
|
||||
**Critical Finding: Overlay Communication is Fundamentally Flawed**
|
||||
|
||||
The existing `flutter_overlay_window` implementation has documented reliability problems:
|
||||
|
||||
1. **Data Sync Issues:** [GitHub issue #115](https://github.com/X-SLAYER/flutter_overlay_window/issues/115) documents "can't display my data on the overlay window" - matches project's "seriously buggy" assessment.
|
||||
2. **Hardware Rendering Glitches:** Flutter 3.27+ causes graphic glitches with overlays on Samsung devices, requiring "Disable HW Overlays" workaround.
|
||||
3. **Android Version Inconsistency:** Android 11+ shows notification bubbles instead of overlays on some configurations.
|
||||
|
||||
**Recommended Strategy: Isolate and Stabilize, Not Replace**
|
||||
|
||||
**WHY NOT switch packages:**
|
||||
- **system_alert_window:** Same underlying Android limitations, different API. No reliability gain.
|
||||
- **flutter_overlay_window_plus:** "Improved API" but no evidence of fixing fundamental IPC issues.
|
||||
- **Native Android implementation:** Massive scope creep, requires platform channel expertise.
|
||||
|
||||
**INSTEAD: Architectural Isolation Pattern**
|
||||
|
||||
```
|
||||
Main App (Foreground)
|
||||
├── GPS Hardware ← Single source of truth
|
||||
├── GpsDataManager (Singleton) ← Processes data
|
||||
├── UI Widgets (Consumer) ← Display main UI
|
||||
└── Overlay Messaging (Fire-and-forget) ← One-way updates only
|
||||
|
||||
Overlay Window (Background isolate)
|
||||
├── Receives messages via FlutterOverlayWindow.overlayListener
|
||||
├── Displays last-received GPS data (read-only)
|
||||
└── Close button → Sends termination signal
|
||||
```
|
||||
|
||||
**Key Pattern: One-Way Data Flow with Defensive Fallbacks**
|
||||
|
||||
```dart
|
||||
// In main app: Fire-and-forget messaging
|
||||
void _sendGpsDataToOverlay(ProcessedGpsData data) {
|
||||
if (_isOverlayActive) {
|
||||
try {
|
||||
FlutterOverlayWindow.shareData({
|
||||
'speed': data.displaySpeed,
|
||||
'unit': _currentUnit.symbol,
|
||||
'heading': data.displayHeading,
|
||||
});
|
||||
} catch (e) {
|
||||
// Fail silently - overlay updates are nice-to-have
|
||||
debugPrint('Overlay message failed: $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// In overlay: Defensive data handling
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return StreamBuilder<dynamic>(
|
||||
stream: FlutterOverlayWindow.overlayListener,
|
||||
initialData: {'speed': '--', 'unit': 'km/h', 'heading': 'N/A'},
|
||||
builder: (context, snapshot) {
|
||||
final data = snapshot.data ?? {};
|
||||
return Text('${data['speed'] ?? '--'} ${data['unit'] ?? ''}');
|
||||
},
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
**What NOT to do:**
|
||||
- ❌ Bidirectional communication (overlay → main app requests)
|
||||
- ❌ Shared mutable state between isolates
|
||||
- ❌ Complex UI in overlay (keep it minimal: speed + close button)
|
||||
- ❌ Rely on overlay for critical functionality
|
||||
|
||||
**Confidence:** MEDIUM (Based on [GitHub issues](https://github.com/X-SLAYER/flutter_overlay_window/issues) and [pub.dev package page](https://pub.dev/packages/flutter_overlay_window))
|
||||
|
||||
### Power Management
|
||||
| Technology | Version | Purpose | Rationale |
|
||||
|------------|---------|---------|-----------|
|
||||
| wakelock_plus | 1.2.8 | Keep screen on | Already integrated. Prevents screen sleep during active navigation. |
|
||||
|
||||
**Power Optimization Pattern for Mixed-Use (Car + Battery):**
|
||||
|
||||
```dart
|
||||
// Conditional GPS tracking based on overlay visibility
|
||||
void _handleOverlayStateChange(bool overlayVisible) {
|
||||
if (overlayVisible) {
|
||||
// Start background GPS with BALANCED accuracy
|
||||
_gpsManager.startBackgroundTracking(LocationAccuracy.medium);
|
||||
WakelockPlus.enable(); // Prevent sleep
|
||||
} else {
|
||||
// Stop GPS when overlay hidden + app backgrounded
|
||||
if (_appLifecycleState == AppLifecycleState.paused) {
|
||||
_gpsManager.stopTracking();
|
||||
WakelockPlus.disable();
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Android GPS Power Best Practices:**
|
||||
1. **Use FusedLocationProvider:** Intelligently combines GPS, Wi-Fi, cell towers. geolocator uses this by default.
|
||||
2. **Maximize intervalDuration:** Pass largest possible interval value for foreground use cases (1-2 seconds acceptable).
|
||||
3. **Batch updates:** Use `AndroidSettings.maxUpdateDelayMillis()` to batch multiple location updates (delay 3-5x the interval).
|
||||
4. **Switch to BALANCED_POWER_ACCURACY when stationary:** Reserve HIGH_ACCURACY for movement.
|
||||
5. **Stop updates when unnecessary:** Remove location listeners in `onPause()`, restore in `onResume()`.
|
||||
|
||||
**Why this matters for GPS speedometer:**
|
||||
- Car use: Plugged in, no battery concern → HIGH_ACCURACY always acceptable
|
||||
- Battery cycling (parking → driving): Must stop GPS when parked, restart on movement
|
||||
- Overlay visible = user expects real-time data = justify GPS power drain
|
||||
|
||||
**Confidence:** HIGH (Based on [Android battery optimization docs](https://developer.android.com/develop/sensors-and-location/location/battery) and [Google Maps SDK power guide](https://developers.google.com/maps/documentation/navigation/android-sdk/optimize-power))
|
||||
|
||||
---
|
||||
|
||||
## Architecture Stack
|
||||
|
||||
### Recommended Pattern: MVVM (Simplified)
|
||||
|
||||
**Based on [Flutter official architecture guide](https://docs.flutter.dev/app-architecture/guide):**
|
||||
|
||||
```
|
||||
UI Layer:
|
||||
├── Views (Widgets) ← Minimal logic, display only
|
||||
└── View Models (ChangeNotifier) ← UI state + user interaction commands
|
||||
|
||||
Data Layer:
|
||||
├── Repositories ← Data source of truth (GPS, settings)
|
||||
└── Services ← External API wrappers (Geolocator, SharedPreferences)
|
||||
```
|
||||
|
||||
**No Domain Layer Needed:** Use-cases/interactors are overkill for single-screen GPS app. Only add domain layer if merging multiple complex repositories.
|
||||
|
||||
**Component Boundaries:**
|
||||
|
||||
| Component | Responsibility | Communicates With |
|
||||
|-----------|---------------|-------------------|
|
||||
| **SpeedometerScreen** (View) | Widget tree, user input handling | SpeedometerViewModel only |
|
||||
| **SpeedometerViewModel** (ChangeNotifier) | UI state (theme, unit, display mode), user commands | GpsRepository, SettingsRepository |
|
||||
| **GpsRepository** | GPS data stream, speed/heading processing | GpsService, ProcessedGpsData model |
|
||||
| **SettingsRepository** | Persistent settings (theme, unit) | SettingsService (SharedPreferences) |
|
||||
| **GpsService** | Geolocator API wrapper, permission handling | Android GPS hardware |
|
||||
| **OverlayMessenger** (Optional service) | One-way data to overlay isolate | flutter_overlay_window |
|
||||
|
||||
**Critical: Existing GpsDataManager Must Be Refactored**
|
||||
|
||||
Current implementation mixes responsibilities:
|
||||
- ✅ **Correct:** Singleton pattern, broadcast stream, stale data detection
|
||||
- ❌ **Wrong:** Directly processes UI display logic (`displaySpeed`, `displayHeading`)
|
||||
- ❌ **Wrong:** Hardcoded m/s → display conversion (should be repository concern)
|
||||
|
||||
**Refactor Strategy:**
|
||||
|
||||
```dart
|
||||
// BEFORE (Current GpsDataManager) - Service with UI logic
|
||||
class GpsDataManager {
|
||||
ProcessedGpsData _currentData; // Contains displaySpeed, displayHeading
|
||||
String getFormattedSpeed(SpeedUnit unit) { /* UI formatting */ }
|
||||
}
|
||||
|
||||
// AFTER (Clean Architecture)
|
||||
// 1. GpsService (Pure data layer) - Just wraps Geolocator
|
||||
class GpsService {
|
||||
Stream<Position> get positionStream => Geolocator.getPositionStream(...);
|
||||
static Future<bool> requestPermissions() => ...;
|
||||
}
|
||||
|
||||
// 2. GpsRepository (Data processing) - Converts Position → domain model
|
||||
class GpsRepository {
|
||||
Stream<GpsData> get dataStream => _gpsService.positionStream
|
||||
.map((position) => GpsData(
|
||||
speedMps: position.speed,
|
||||
headingDegrees: position.heading,
|
||||
isValid: position.speed.isFinite,
|
||||
));
|
||||
}
|
||||
|
||||
// 3. SpeedometerViewModel (UI logic) - Formats for display
|
||||
class SpeedometerViewModel extends ChangeNotifier {
|
||||
String get displaySpeed {
|
||||
final data = _gpsRepository.currentData;
|
||||
if (!data.isValid || data.speedMps < 0.3) return '--';
|
||||
return _currentUnit.convert(data.speedMps).toStringAsFixed(1);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Confidence:** HIGH (Official Flutter architecture guide)
|
||||
|
||||
---
|
||||
|
||||
## Directory Structure: Layer-First (Recommended for This Project)
|
||||
|
||||
**Feature-first vs Layer-first decision:**
|
||||
|
||||
| Approach | Best For | This Project? |
|
||||
|----------|----------|---------------|
|
||||
| **Feature-first** | Multi-feature apps (e.g., e-commerce: cart, checkout, profile) | ❌ No - Single-screen GPS app |
|
||||
| **Layer-first** | Apps with few features but complex layers | ✅ Yes - One feature, multiple concerns (GPS, overlay, settings) |
|
||||
|
||||
**Recommended Structure:**
|
||||
|
||||
```
|
||||
lib/
|
||||
├── main.dart # App entry point + overlay entry point
|
||||
├── screens/ # UI Layer - Views only
|
||||
│ ├── speedometer_screen.dart # Main screen widget tree
|
||||
│ └── overlay_screen.dart # Overlay isolate UI (if separated)
|
||||
├── viewmodels/ # UI Layer - View models (ChangeNotifier)
|
||||
│ └── speedometer_viewmodel.dart
|
||||
├── widgets/ # Reusable UI components
|
||||
│ ├── speed_display.dart # Digital display widget
|
||||
│ ├── gauge_widget.dart # Analog gauge widget
|
||||
│ └── metrics_panel.dart # Optional metrics display
|
||||
├── repositories/ # Data Layer - Data processing
|
||||
│ ├── gps_repository.dart # GPS data stream + processing
|
||||
│ └── settings_repository.dart # Persistent settings (theme, unit)
|
||||
├── services/ # Data Layer - External API wrappers
|
||||
│ ├── gps_service.dart # Geolocator wrapper
|
||||
│ ├── settings_service.dart # SharedPreferences wrapper
|
||||
│ └── overlay_messenger.dart # One-way overlay communication (optional)
|
||||
├── models/ # Data models (pure data classes)
|
||||
│ ├── gps_data.dart # Domain model for GPS data
|
||||
│ └── app_settings.dart # Domain model for settings
|
||||
├── utils/ # Pure functions, constants
|
||||
│ ├── speed_units.dart # SpeedUnit enum + conversion logic
|
||||
│ └── color_themes.dart # Theme definitions
|
||||
└── constants/ # App-wide constants
|
||||
└── app_constants.dart # Timeouts, thresholds, etc.
|
||||
```
|
||||
|
||||
**Migration Path from Current Monolith:**
|
||||
|
||||
1. **Phase 1:** Extract models (`gps_data.dart`, `app_settings.dart`)
|
||||
2. **Phase 2:** Create services (wrap existing `GpsService`, create `SettingsService`)
|
||||
3. **Phase 3:** Create repositories (move business logic from `GpsDataManager`)
|
||||
4. **Phase 4:** Create viewmodel (extract state from `_SpeedometerScreenState`)
|
||||
5. **Phase 5:** Refactor screen to pure widget (remove setState, use Consumer)
|
||||
|
||||
**What NOT to do:**
|
||||
- ❌ Create `helpers/` or `utils/` catch-all folders for business logic
|
||||
- ❌ Mix UI and business logic in viewmodels
|
||||
- ❌ Put repositories in `providers/` folder (use `repositories/`)
|
||||
- ❌ Create deep feature hierarchies (`features/speedometer/screens/main/widgets/...`)
|
||||
|
||||
**Confidence:** HIGH (Based on [Flutter project structure guide](https://codewithandrea.com/articles/flutter-project-structure/) and [official architecture docs](https://docs.flutter.dev/app-architecture/guide))
|
||||
|
||||
---
|
||||
|
||||
## Provider Patterns: Performance Best Practices
|
||||
|
||||
### ChangeNotifier Usage
|
||||
|
||||
**Core Pattern:**
|
||||
|
||||
```dart
|
||||
class SpeedometerViewModel extends ChangeNotifier {
|
||||
final GpsRepository _gpsRepository;
|
||||
final SettingsRepository _settingsRepository;
|
||||
StreamSubscription<GpsData>? _gpsSubscription;
|
||||
|
||||
SpeedometerViewModel(this._gpsRepository, this._settingsRepository) {
|
||||
_gpsSubscription = _gpsRepository.dataStream.listen(_onGpsUpdate);
|
||||
_loadSettings();
|
||||
}
|
||||
|
||||
void _onGpsUpdate(GpsData data) {
|
||||
// Update internal state
|
||||
_currentGpsData = data;
|
||||
notifyListeners(); // Trigger UI rebuild
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_gpsSubscription?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Critical Performance Rules:**
|
||||
|
||||
1. **Place Consumer deep in widget tree:**
|
||||
```dart
|
||||
// ❌ BAD - Rebuilds entire screen
|
||||
return Consumer<SpeedometerViewModel>(
|
||||
builder: (context, vm, child) => Scaffold(...),
|
||||
);
|
||||
|
||||
// ✅ GOOD - Rebuilds only speed text
|
||||
return Scaffold(
|
||||
body: Column(
|
||||
children: [
|
||||
StaticHeader(),
|
||||
Consumer<SpeedometerViewModel>(
|
||||
builder: (context, vm, child) => Text(vm.displaySpeed),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
```
|
||||
|
||||
2. **Use `listen: false` when not subscribing:**
|
||||
```dart
|
||||
// ✅ In event handlers (button presses)
|
||||
void _onThemeButtonPressed() {
|
||||
final vm = Provider.of<SpeedometerViewModel>(context, listen: false);
|
||||
vm.nextTheme(); // Just calling a command, not listening to changes
|
||||
}
|
||||
|
||||
// Or use context.read (same thing)
|
||||
void _onThemeButtonPressed() {
|
||||
context.read<SpeedometerViewModel>().nextTheme();
|
||||
}
|
||||
```
|
||||
|
||||
3. **Use `child` parameter for static subtrees:**
|
||||
```dart
|
||||
Consumer<SpeedometerViewModel>(
|
||||
child: ExpensiveStaticWidget(), // Built once, passed to builder
|
||||
builder: (context, vm, staticChild) => Column(
|
||||
children: [
|
||||
Text(vm.displaySpeed), // Rebuilds
|
||||
staticChild!, // Never rebuilds
|
||||
],
|
||||
),
|
||||
);
|
||||
```
|
||||
|
||||
4. **Use Selector for granular rebuilds:**
|
||||
```dart
|
||||
// Only rebuilds when displaySpeed changes, not on theme/unit changes
|
||||
Selector<SpeedometerViewModel, String>(
|
||||
selector: (context, vm) => vm.displaySpeed,
|
||||
builder: (context, displaySpeed, child) => Text(displaySpeed),
|
||||
);
|
||||
```
|
||||
|
||||
5. **Always call `notifyListeners()` after state changes:**
|
||||
```dart
|
||||
void setUnit(SpeedUnit unit) {
|
||||
_currentUnit = unit;
|
||||
notifyListeners(); // Critical - UI won't update without this
|
||||
}
|
||||
```
|
||||
|
||||
6. **NEVER use `context.read()` inside build():**
|
||||
```dart
|
||||
// ❌ BAD - Won't rebuild on changes
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final vm = context.read<SpeedometerViewModel>();
|
||||
return Text(vm.displaySpeed); // Stale data!
|
||||
}
|
||||
|
||||
// ✅ GOOD - Subscribes to changes
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final vm = context.watch<SpeedometerViewModel>();
|
||||
return Text(vm.displaySpeed);
|
||||
}
|
||||
```
|
||||
|
||||
**Provider Setup in main.dart:**
|
||||
|
||||
```dart
|
||||
void main() {
|
||||
runApp(
|
||||
MultiProvider(
|
||||
providers: [
|
||||
// Services (no state, just API wrappers)
|
||||
Provider(create: (_) => GpsService()),
|
||||
Provider(create: (_) => SettingsService()),
|
||||
|
||||
// Repositories (depend on services)
|
||||
ProxyProvider<GpsService, GpsRepository>(
|
||||
update: (_, gpsService, __) => GpsRepository(gpsService),
|
||||
),
|
||||
ProxyProvider<SettingsService, SettingsRepository>(
|
||||
update: (_, settingsService, __) => SettingsRepository(settingsService),
|
||||
),
|
||||
|
||||
// ViewModels (depend on repositories)
|
||||
ChangeNotifierProxyProvider2<GpsRepository, SettingsRepository, SpeedometerViewModel>(
|
||||
create: (_) => SpeedometerViewModel(
|
||||
Provider.of<GpsRepository>(_, listen: false),
|
||||
Provider.of<SettingsRepository>(_, listen: false),
|
||||
),
|
||||
update: (_, gpsRepo, settingsRepo, vm) =>
|
||||
vm ?? SpeedometerViewModel(gpsRepo, settingsRepo),
|
||||
),
|
||||
],
|
||||
child: const SpeedoApp(),
|
||||
),
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
**Confidence:** HIGH (Based on [official Flutter Provider docs](https://docs.flutter.dev/data-and-backend/state-mgmt/simple) and [Provider performance guide](https://www.dhiwise.com/post/achieving-optimal-performance-with-flutter-provider-state-management))
|
||||
|
||||
---
|
||||
|
||||
## Installation & Dependency Changes
|
||||
|
||||
**No new packages needed.** All dependencies already in pubspec.yaml.
|
||||
|
||||
### Current Dependencies (Keep As-Is)
|
||||
```yaml
|
||||
dependencies:
|
||||
flutter:
|
||||
sdk: flutter
|
||||
geolocator: ^14.0.2 # Already integrated
|
||||
permission_handler: ^12.0.1 # Already integrated
|
||||
wakelock_plus: ^1.2.8 # Already integrated
|
||||
flutter_overlay_window: ^0.5.0 # Already integrated (problematic but keep)
|
||||
bg_launcher: ^0.1.0 # Already integrated
|
||||
# provider: ^6.1.x # MISSING - Add this line
|
||||
```
|
||||
|
||||
### Required Addition
|
||||
```bash
|
||||
flutter pub add provider
|
||||
```
|
||||
|
||||
### Dev Dependencies (Keep As-Is)
|
||||
```yaml
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
sdk: flutter
|
||||
flutter_lints: ^6.0.0
|
||||
flutter_launcher_icons: "^0.14.4"
|
||||
```
|
||||
|
||||
**What NOT to add:**
|
||||
- ❌ Riverpod (scope creep, Provider is sufficient)
|
||||
- ❌ get_it (dependency injection - Provider handles this)
|
||||
- ❌ freezed (immutable models - overkill for simple data classes)
|
||||
- ❌ Alternative overlay packages (same limitations as current)
|
||||
|
||||
---
|
||||
|
||||
## Anti-Patterns: What NOT to Do
|
||||
|
||||
### 1. Don't Over-Abstract
|
||||
```dart
|
||||
// ❌ BAD - Premature abstraction
|
||||
abstract class LocationDataSource {
|
||||
Stream<Position> getPositionStream();
|
||||
}
|
||||
class GeolocatorDataSource implements LocationDataSource { ... }
|
||||
|
||||
// ✅ GOOD - Direct service wrapper
|
||||
class GpsService {
|
||||
Stream<Position> get positionStream => Geolocator.getPositionStream(...);
|
||||
}
|
||||
```
|
||||
|
||||
**Why:** You're not swapping out Geolocator. YAGNI principle applies.
|
||||
|
||||
### 2. Don't Mix UI Logic in Repositories
|
||||
```dart
|
||||
// ❌ BAD - Repository formats for UI
|
||||
class GpsRepository {
|
||||
String get displaySpeed => _formatForUi(_currentSpeed);
|
||||
}
|
||||
|
||||
// ✅ GOOD - Repository provides domain data
|
||||
class GpsRepository {
|
||||
double get speedMps => _currentSpeed;
|
||||
}
|
||||
class SpeedometerViewModel {
|
||||
String get displaySpeed => _formatForUi(_gpsRepository.speedMps);
|
||||
}
|
||||
```
|
||||
|
||||
**Why:** Repositories are reusable across features. UI formatting is view-specific.
|
||||
|
||||
### 3. Don't Create God ViewModels
|
||||
```dart
|
||||
// ❌ BAD - Single viewmodel manages everything
|
||||
class AppViewModel extends ChangeNotifier {
|
||||
GpsData gpsData;
|
||||
AppSettings settings;
|
||||
bool isOverlayActive;
|
||||
// ... 50 more properties
|
||||
}
|
||||
|
||||
// ✅ GOOD - Separate concerns
|
||||
class SpeedometerViewModel extends ChangeNotifier { /* GPS + UI state */ }
|
||||
class SettingsViewModel extends ChangeNotifier { /* Settings only */ }
|
||||
```
|
||||
|
||||
**Why:** For single-screen app, one viewmodel is OK. But separate GPS from settings concerns.
|
||||
|
||||
### 4. Don't Use Dynamic Accuracy Switching Without Hysteresis
|
||||
```dart
|
||||
// ❌ BAD - Constantly switches accuracy as speed fluctuates
|
||||
LocationSettings _getSettings(double speed) {
|
||||
return AndroidSettings(
|
||||
accuracy: speed > 3.0
|
||||
? LocationAccuracy.high // 10.8 km/h
|
||||
: LocationAccuracy.medium,
|
||||
);
|
||||
}
|
||||
|
||||
// ✅ GOOD - Hysteresis prevents thrashing
|
||||
LocationSettings _getSettings(double speed, bool currentlyHighAccuracy) {
|
||||
final threshold = currentlyHighAccuracy ? 2.5 : 3.5; // 9-12.6 km/h range
|
||||
return AndroidSettings(
|
||||
accuracy: speed > threshold
|
||||
? LocationAccuracy.high
|
||||
: LocationAccuracy.medium,
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
**Why:** GPS speed fluctuates ±0.5 m/s. Without hysteresis, you'll rapidly switch accuracy modes, wasting battery and causing UI jank.
|
||||
|
||||
### 5. Don't Rely on Overlay for Critical Features
|
||||
```dart
|
||||
// ❌ BAD - Overlay failure breaks app
|
||||
void _sendGpsUpdate(GpsData data) {
|
||||
if (_isOverlayActive) {
|
||||
FlutterOverlayWindow.shareData(data.toJson()); // May throw
|
||||
} else {
|
||||
throw Exception('Overlay not active!'); // App crashes
|
||||
}
|
||||
}
|
||||
|
||||
// ✅ GOOD - Overlay is optional enhancement
|
||||
void _sendGpsUpdate(GpsData data) {
|
||||
if (_isOverlayActive) {
|
||||
try {
|
||||
FlutterOverlayWindow.shareData(data.toJson());
|
||||
} catch (e) {
|
||||
debugPrint('Overlay update failed, continuing: $e');
|
||||
}
|
||||
}
|
||||
// Main app always works, overlay is bonus
|
||||
}
|
||||
```
|
||||
|
||||
**Why:** Overlay communication is unreliable. Main app must function perfectly without it.
|
||||
|
||||
---
|
||||
|
||||
## Testing Strategy
|
||||
|
||||
### Unit Test Priorities
|
||||
|
||||
1. **Speed unit conversions** (`speed_units.dart`) - Pure functions, easy to test
|
||||
2. **GPS data processing** (`GpsRepository`) - Mock Position stream
|
||||
3. **Settings persistence** (`SettingsRepository`) - Mock SharedPreferences
|
||||
4. **ViewModel commands** (`SpeedometerViewModel`) - Mock repositories
|
||||
|
||||
### Widget Test Priorities
|
||||
|
||||
1. **Speed display updates** - Verify Consumer rebuilds on GPS changes
|
||||
2. **Theme switching** - Verify UI updates on theme change
|
||||
3. **Unit switching** - Verify speed conversion displayed correctly
|
||||
|
||||
### Integration Test Priorities
|
||||
|
||||
1. **GPS permission flow** - Verify permission request → GPS stream start
|
||||
2. **Overlay launch/close** - Verify overlay lifecycle doesn't crash main app
|
||||
|
||||
**What NOT to test:**
|
||||
- ❌ Geolocator library internals (trust the library)
|
||||
- ❌ Overlay message delivery (too unreliable, mock it)
|
||||
- ❌ Android system behaviors (permission dialogs, etc.)
|
||||
|
||||
---
|
||||
|
||||
## Sources
|
||||
|
||||
### Official Documentation
|
||||
- [Flutter App Architecture Guide](https://docs.flutter.dev/app-architecture/guide) - MVVM, layer separation
|
||||
- [Flutter State Management: Simple (Provider)](https://docs.flutter.dev/data-and-backend/state-mgmt/simple) - Official Provider guidance
|
||||
- [Geolocator Flutter Package](https://pub.dev/packages/geolocator) - GPS API documentation
|
||||
- [Android Location Request Updates](https://developer.android.com/develop/sensors-and-location/location/request-updates) - Native Android GPS API
|
||||
- [Android Location Battery Optimization](https://developer.android.com/develop/sensors-and-location/location/battery) - Power consumption best practices
|
||||
|
||||
### Community Resources
|
||||
- [Flutter Project Structure: Feature-first or Layer-first?](https://codewithandrea.com/articles/flutter-project-structure/) - Directory structure guidance
|
||||
- [Best Flutter State Management Libraries 2026](https://foresightmobile.com/blog/best-flutter-state-management) - Provider vs Riverpod comparison
|
||||
- [Flutter Provider State Management for Optimal Performance](https://www.dhiwise.com/post/achieving-optimal-performance-with-flutter-provider-state-management) - Performance patterns
|
||||
|
||||
### Package Documentation
|
||||
- [flutter_overlay_window pub.dev](https://pub.dev/packages/flutter_overlay_window) - Overlay API reference
|
||||
- [flutter_overlay_window GitHub Issues](https://github.com/X-SLAYER/flutter_overlay_window/issues) - Known reliability problems
|
||||
- [LocationAccuracy enum API](https://pub.dev/documentation/geolocator_platform_interface/latest/geolocator_platform_interface/LocationAccuracy.html) - Android mapping table
|
||||
|
||||
### Architecture References
|
||||
- [Architecture recommendations and resources](https://docs.flutter.dev/app-architecture/recommendations) - Flutter team's architectural guidance
|
||||
- [Common architecture concepts](https://docs.flutter.dev/app-architecture/concepts) - Layer definitions
|
||||
|
||||
---
|
||||
|
||||
## Confidence Assessment
|
||||
|
||||
| Area | Confidence | Rationale |
|
||||
|------|------------|-----------|
|
||||
| **Provider patterns** | HIGH | Official Flutter docs, mature package (6.1.x), well-documented |
|
||||
| **MVVM architecture** | HIGH | Official Flutter architecture guide 2026 |
|
||||
| **Geolocator adaptive GPS** | HIGH | Official Android docs + geolocator 14.0.2 API verified |
|
||||
| **Android power optimization** | HIGH | Official Android developer docs for 2026 |
|
||||
| **Overlay reliability** | MEDIUM | GitHub issues + community reports confirm problems, but no authoritative solution found |
|
||||
| **Directory structure** | HIGH | Layer-first appropriate for single-screen app per community best practices |
|
||||
|
||||
---
|
||||
|
||||
## Open Questions / Phase-Specific Research Needed
|
||||
|
||||
1. **Overlay Performance Impact:** Quantify actual battery/CPU impact of running overlay isolate on various Android versions. May need device-specific testing.
|
||||
|
||||
2. **GPS Accuracy Hysteresis Tuning:** Optimal speed thresholds for HIGH ↔ BALANCED transitions depend on real-world testing. Starting recommendation: 2.5-3.5 m/s (~9-12.6 km/h) with 1.0 m/s deadband.
|
||||
|
||||
3. **Android 14+ Foreground Service Policies:** Verify FOREGROUND_SERVICE_LOCATION permission behavior on Android 14+ devices. May require additional AndroidManifest.xml configuration beyond current setup.
|
||||
|
||||
4. **Provider Memory Leaks:** Verify ChangeNotifier disposal in overlay isolate. Isolates don't share memory, so dispose() semantics may differ. Test with DevTools memory profiler.
|
||||
|
||||
---
|
||||
|
||||
**Next Steps:** Feed this STACK.md into roadmap creation to structure phases around:
|
||||
1. Provider migration (extract viewmodels)
|
||||
2. Repository pattern implementation (GpsRepository, SettingsRepository)
|
||||
3. Adaptive GPS precision (speed-based LocationSettings)
|
||||
4. Overlay isolation (one-way messaging, defensive error handling)
|
||||
5. Power optimization (lifecycle-aware GPS control)
|
||||
@@ -0,0 +1,297 @@
|
||||
# Project Research Summary
|
||||
|
||||
**Project:** GPS Speedometer - Monolith to Modular Architecture Restructure
|
||||
**Domain:** Flutter mobile application restructuring
|
||||
**Researched:** 2026-02-09
|
||||
**Confidence:** HIGH
|
||||
|
||||
## Executive Summary
|
||||
|
||||
The GPS Speedometer app requires restructuring from a 1,133-line monolithic main.dart with raw setState into a modern 2026 Flutter architecture using Provider state management, MVVM layer separation, and feature-based organization. Research reveals that the current approach is 2018-era Flutter that suffers from untestable business logic, memory leaks from uncanceled subscriptions, and aggressive battery drain from continuous HIGH_ACCURACY GPS and wake locks. The recommended path forward prioritizes architectural foundation first (Provider migration, widget extraction, service layer), then GPS optimization (adaptive precision, lifecycle-aware tracking), and finally overlay communication fixes (message protocol, isolate-aware state management).
|
||||
|
||||
The critical insight is that this is fundamentally a restructuring project, not a feature-addition project. The restructure must preserve all existing functionality while enabling future maintainability. The highest risks cluster around Provider migration breaking async lifecycle patterns (setState after dispose, stream subscription leaks), and overlay communication becoming unreliable due to isolate boundaries and fragile message passing. Both have clear prevention strategies: audit all async patterns before migration, use typed message schemas, and implement defensive error handling throughout.
|
||||
|
||||
Success requires disciplined manual regression testing (no automated tests exist), careful attention to Android lifecycle management (backgrounding, permission revocation, GPS duty-cycling), and battery optimization to avoid Google Play Store penalties for excessive wake lock usage (new enforcement starting 2026). The restructure is estimated at 52-70 hours with Phase 2 (Provider Migration) and Phase 3 (Overlay Refactor) carrying the highest regression risk.
|
||||
|
||||
## Key Findings
|
||||
|
||||
### Recommended Stack
|
||||
|
||||
The restructure requires NO new major dependencies beyond adding `provider: ^6.1.0` which is already in pubspec.yaml but not fully utilized. The existing stack (Flutter 3.24+, geolocator 14.0.2, flutter_overlay_window 0.5.0, wakelock_plus 1.2.8) is adequate but used incorrectly. The focus is leveraging existing packages properly rather than introducing new ones.
|
||||
|
||||
**Core technologies:**
|
||||
- **Provider 6.1.x**: UI state management — Official Flutter recommendation for simple-to-moderate state. Already in dependencies but unused. Replaces raw setState with ChangeNotifier pattern for reactive UI updates. Sufficient for app's complexity (Riverpod migration would be scope creep despite community preference in 2026).
|
||||
- **Geolocator 14.0.2**: GPS position stream — Already integrated with Android 14+ FOREGROUND_SERVICE_LOCATION support. Critical feature: LocationSettings for adaptive precision control (HIGH_ACCURACY when moving fast, BALANCED_POWER_ACCURACY when stationary). Must use AndroidSettings.intervalDuration pattern (old desiredAccuracy deprecated).
|
||||
- **flutter_overlay_window 0.5.0**: System-level overlay — Already integrated but PROBLEMATIC. Known reliability issues with bidirectional communication, data staleness, and Android version inconsistency. Keep package but isolate with architectural patterns: one-way message flow, typed schemas, defensive error handling. Do NOT replace (alternatives have same limitations).
|
||||
- **MVVM Architecture Pattern**: Layer separation — Official Flutter 2026 architecture guide. View → ViewModel → Repository → Service. No domain layer needed (single-screen app). Layer-first directory structure appropriate (feature-first overkill for limited features).
|
||||
|
||||
**Critical stack decision:** Use Provider with existing packages, not BLoC (overkill) or Riverpod (scope creep). Isolate overlay communication issues architecturally (typed messages, one-way flow) rather than switching packages.
|
||||
|
||||
### Expected Features
|
||||
|
||||
This is a restructuring project focused on HOW features are structured, not WHAT features to add. The goal is preserving existing functionality while enabling maintainability, testability, and battery optimization.
|
||||
|
||||
**Must have (table stakes - preserve existing):**
|
||||
- MVVM layer separation — View/ViewModel/Repository/Service distinct responsibilities
|
||||
- Provider integration — Replace setState with ChangeNotifier + Consumer pattern
|
||||
- Repository pattern — GpsDataManager as single source of truth for GPS data
|
||||
- Unidirectional data flow — Data flows down (Service → Repository → ViewModel → View), events flow up
|
||||
- Lifecycle-aware state management — Handle app backgrounding, overlay launch/close, GPS stream lifecycle properly
|
||||
- Isolate-aware overlay communication — Structured message protocol for main ↔ overlay isolate
|
||||
- Service layer isolation — GPS hardware access only through service layer
|
||||
|
||||
**Should have (competitive - new optimizations):**
|
||||
- Speed-adaptive GPS precision — Switch HIGH_ACCURACY ↔ BALANCED_POWER_ACCURACY based on movement (e.g., >12 km/h = HIGH, <8 km/h = BALANCED with hysteresis). Critical for battery life.
|
||||
- WorkManager background tasks — Modern alternative to aggressive wake locks for system-managed power
|
||||
- Message queue for overlay sync — Continuous broadcast stream to overlay (debounced 2-4 updates/sec) to fix stale data bug
|
||||
- Scoped Provider instances — Separate Provider tree per isolate (main app vs overlay) to prevent state collision
|
||||
- Foreground service architecture — Proper Android foreground service with notification (prevents system kill)
|
||||
- Multi-repository ViewModel — SpeedometerViewModel composes GpsRepository + SettingsRepository + OverlayRepository
|
||||
|
||||
**Defer (v2+ - not essential for restructure):**
|
||||
- Duty-cycle awareness — Buffer GPS readings for smooth UI when Android duty-cycles hardware
|
||||
- Hybrid GPS strategy — Combine speed threshold + WiFi state + motion sensors for advanced optimization
|
||||
- Over-engineering with BLoC — Current team using Provider, BLoC adds boilerplate without benefit
|
||||
|
||||
**Anti-patterns to avoid:**
|
||||
- Monolithic ViewModel/Widget — Current 1,133-line main.dart must be split into feature-specific ViewModels (<200 lines each)
|
||||
- Shared state between isolates — Overlay cannot access main app memory, use message passing only
|
||||
- Continuous HIGH_ACCURACY GPS — Current battery drain (7-12%/hour) violates Google Play policy
|
||||
- Wake lock without justification — Permanent wake lock triggers Google Play penalties (>2 hours in 24h threshold)
|
||||
- setState for cross-widget state — Forces entire screen rebuilds, doesn't scale to overlay sync
|
||||
|
||||
### Architecture Approach
|
||||
|
||||
Flutter GPS speedometer apps require clean separation between UI, state management, GPS data processing, and overlay communication. The recommended architecture follows Flutter's MVVM pattern with Provider state management, modular widget extraction by responsibility, and a dedicated overlay service for reliable system-level window communication.
|
||||
|
||||
**Layer structure:**
|
||||
1. **UI Layer (Views)** — screens/ and widgets/, StatelessWidgets using Consumer, no business logic
|
||||
2. **State Management (ViewModels)** — providers/ with ChangeNotifier, subscribe to services, transform data for UI, expose commands
|
||||
3. **Services (Data/Logic)** — services/ wrapping external APIs (GPS, overlay, settings), provide streams/futures, handle lifecycle
|
||||
4. **Models (Domain)** — models/ with immutable data classes, copyWith/toJson/fromJson, no business logic
|
||||
5. **Configuration** — config/ with constants, enums, themes
|
||||
|
||||
**Data flow pattern (unidirectional):**
|
||||
```
|
||||
GPS Hardware → GpsDataManager (Singleton) → SpeedometerProvider (ChangeNotifier)
|
||||
→ Consumer Widgets → OverlayProvider → OverlayService → Overlay Window (Separate Isolate)
|
||||
```
|
||||
|
||||
**Major components:**
|
||||
1. **GpsDataManager (Service)** — Singleton managing GPS stream subscription, processes raw Position into ProcessedGpsData, broadcasts via Stream, handles staleness detection. KEEP AS-IS (already correctly implemented).
|
||||
2. **SpeedometerProvider (ViewModel)** — ChangeNotifier subscribing to GpsDataManager.dataStream, transforms data to UI-ready strings (displaySpeed, displayHeading), manages trip stats, coordinates overlay updates, calls notifyListeners() on changes.
|
||||
3. **OverlayService (Service)** — Encapsulates flutter_overlay_window platform channel calls, manages lifecycle (show/close), status stream for reactive state, graceful error handling, heartbeat for status polling (overlay in separate isolate).
|
||||
4. **Widget Extraction** — SpeedometerScreen broken into leaf widgets (SpeedDisplay, HeadingCompass, UnitSelector, MetricsPanel) with internal Consumers for granular rebuilds.
|
||||
5. **Typed Overlay Messages** — OverlayMessage class with factory constructors (speedUpdate, themeUpdate, close) for type-safe message passing between isolates.
|
||||
|
||||
**Key architectural decisions:**
|
||||
- Provider at top level (app-wide singletons for services)
|
||||
- Services before providers (dependency order)
|
||||
- Consumer optimization (place deep in widget tree, use Selector for targeted rebuilds)
|
||||
- Overlay message debouncing (5 Hz instead of GPS rate to prevent channel flooding)
|
||||
- No shared state between main/overlay isolates (message passing only)
|
||||
|
||||
### Critical Pitfalls
|
||||
|
||||
The top 5 pitfalls that cause rewrites, regressions, or production failures:
|
||||
|
||||
1. **setState After Dispose During Provider Migration** — Async operations (Timers, Stream subscriptions, Future callbacks) continue running after widget disposal, causing "setState() called after dispose()" or "ChangeNotifier used after being disposed()" crashes. Current code has 5+ async patterns (background heartbeat, overlay status poll, tap-close timer, stale data timer, GPS subscription). Prevention: Audit all async patterns before migration, always check `mounted` before setState, cancel ALL timers/subscriptions in dispose(), override notifyListeners with disposal check, use ChangeNotifierProvider.value for singletons.
|
||||
|
||||
2. **GPS Stream Subscription Memory Leaks** — GPS stream subscriptions remain active after widget disposal causing memory leaks, battery drain, and indefinite background location requests. GpsDataManager singleton has dispose() but nothing calls it. Prevention: Cancel stream subscriptions in dispose(), store subscriptions when created, implement reference counting or Provider-managed lifecycle for singleton, stop GPS when app backgrounded without overlay active, use Flutter's Leak Tracker during development.
|
||||
|
||||
3. **Overlay Communication Breaking After Modularization** — flutter_overlay_window 0.5.0 has known bidirectional communication issues. After extracting overlay logic to separate service/provider, data stops flowing, overlay shows stale speed, close commands ignored. Root cause: separate isolates with isolated memory, only message passing works, current unvalidated Map data. Prevention: Define typed message schema BEFORE refactoring, validate ALL data at isolate boundaries, surface overlay errors to user (don't swallow), verify overlay creation succeeded before setting flags, avoid HapticFeedback in overlay context (hangs).
|
||||
|
||||
4. **Excessive Battery Drain From Unconditional Background Timers** — Background heartbeat (5s), overlay polling (1s), continuous HIGH_ACCURACY GPS cause Android vitals to flag app for excessive battery drain. Google Play Store enforcement starting March 2026 (>2 hours wake lock in 24h = bad sessions, >5% bad sessions = recommendation exclusion). Prevention: Stop heartbeat when not needed (only run when backgrounded AND overlay active), replace 1s polling with event-based detection or 5s interval, speed-adaptive GPS precision (8/12 km/h hysteresis), stop GPS completely when overlay closed, cache overlay size calculation.
|
||||
|
||||
5. **Singleton Thread Safety Assumptions Breaking With Isolates** — GpsDataManager singleton uses lazy initialization which is NOT thread-safe across isolates. Overlay runs in separate isolate — if it tries accessing singleton directly, creates separate instance (data desync) or race condition. Prevention: Document single-isolate constraint in code comments, use synchronized package if multi-isolate access truly needed (it's not), enforce single-isolate architecture (GPS in main only, overlay receives via messages), add runtime isolate check throwing error if created in wrong isolate.
|
||||
|
||||
**Additional moderate pitfalls:**
|
||||
- ChangeNotifier excessive rebuilds — Every notifyListeners() rebuilds ALL Consumers. Use Selector for targeted rebuilds, place Consumer deep in tree, split large ChangeNotifiers.
|
||||
- Manual regression testing missing edge cases — Zero automated tests means manual testing follows happy path, misses permission revocation, GPS disable/enable races, lifecycle transitions. Create checklist BEFORE restructuring, test on multiple devices/Android versions.
|
||||
- Permission state changes not handled at runtime — User can revoke overlay permission while app running (Android 11+). Check permission before EVERY overlay operation, handle revocation gracefully with user feedback, re-check on app resume.
|
||||
|
||||
## Implications for Roadmap
|
||||
|
||||
Based on research, suggested 4-phase structure with clear dependency chain:
|
||||
|
||||
### Phase 1: Foundation (Code Organization)
|
||||
**Rationale:** Establish architectural foundation without changing behavior. Low regression risk since it's primarily file reorganization and constant extraction. Enables all subsequent phases by creating clear component boundaries.
|
||||
|
||||
**Delivers:**
|
||||
- Layer-first directory structure (screens/, widgets/, providers/, services/, models/, config/)
|
||||
- Extracted models (ProcessedGpsData, OverlayMessage, TripStats, SpeedometerSettings)
|
||||
- Centralized constants file (timer durations, GPS thresholds, overlay dimensions)
|
||||
- Moved existing config (speed_units.dart, color_themes.dart to config/)
|
||||
|
||||
**Addresses Features:**
|
||||
- Foundation for MVVM layer separation (table stakes)
|
||||
- Preparation for service layer isolation (table stakes)
|
||||
|
||||
**Avoids Pitfalls:**
|
||||
- Hardcoded magic numbers scattered during refactor (minor pitfall #10)
|
||||
|
||||
**Research Flags:** None — standard Flutter project organization, well-documented patterns.
|
||||
|
||||
### Phase 2: Provider Migration (State Management)
|
||||
**Rationale:** Replace setState with Provider pattern. CRITICAL RISK phase due to async lifecycle complexity. Must come after Phase 1 (needs directory structure) and before Phase 3 (overlay refactor depends on Provider scoping). This is the make-or-break phase.
|
||||
|
||||
**Delivers:**
|
||||
- SpeedometerProvider (ChangeNotifier managing GPS data, trip stats, unit/theme)
|
||||
- ThemeProvider (ChangeNotifier for theme state)
|
||||
- OverlayProvider (ChangeNotifier wrapping OverlayService)
|
||||
- MultiProvider setup in main.dart
|
||||
- Consumer widgets with optimization (Selector, deep placement)
|
||||
- Proper disposal of all async patterns
|
||||
|
||||
**Addresses Features:**
|
||||
- Provider integration (table stakes)
|
||||
- Unidirectional data flow (table stakes)
|
||||
- Lifecycle-aware state management (table stakes)
|
||||
|
||||
**Avoids Pitfalls:**
|
||||
- setState after dispose during Provider migration (CRITICAL #1)
|
||||
- GPS stream subscription memory leaks (CRITICAL #2)
|
||||
- ChangeNotifier excessive rebuilds (MODERATE #6)
|
||||
- Manual regression testing gaps (MODERATE #7)
|
||||
|
||||
**Research Flags:** HIGH RISK — Requires thorough async pattern audit, manual regression testing with checklist, memory profiling. Use template checklist from PITFALLS.md.
|
||||
|
||||
### Phase 3: Overlay Refactor (Isolate Communication)
|
||||
**Rationale:** Fix overlay data staleness and reliability issues. CRITICAL RISK due to isolate complexity. Must come after Phase 2 (needs Provider scoping) but before Phase 4 (GPS optimization can proceed independently).
|
||||
|
||||
**Delivers:**
|
||||
- OverlayService extracted from SpeedometerScreen
|
||||
- Typed OverlayMessage schema (speedUpdate, themeUpdate, close)
|
||||
- Scoped Provider instances (separate main/overlay trees)
|
||||
- Message queue with debouncing (5 Hz updates)
|
||||
- Status stream for overlay lifecycle tracking
|
||||
- Heartbeat conditional logic (only when backgrounded + overlay active)
|
||||
- Graceful error handling (don't swallow, surface to user)
|
||||
|
||||
**Addresses Features:**
|
||||
- Isolate-aware overlay communication (table stakes)
|
||||
- Message queue for overlay sync (competitive)
|
||||
- Scoped Provider instances (competitive)
|
||||
|
||||
**Avoids Pitfalls:**
|
||||
- Overlay communication breaking after modularization (CRITICAL #3)
|
||||
- Excessive battery drain from timers (CRITICAL #4)
|
||||
- Permission runtime changes not handled (MODERATE #8)
|
||||
|
||||
**Research Flags:** MEDIUM RISK — flutter_overlay_window 0.5.0 has known issues. Define message schema first, validate on multiple Android versions. Consider flutter_overlay_window_plus fork if communication unreliable.
|
||||
|
||||
### Phase 4: GPS Optimization (Battery & Performance)
|
||||
**Rationale:** Implement adaptive GPS precision to address battery drain and Google Play policy compliance. Can proceed independently after Provider migration establishes lifecycle management. Less risky than overlay refactor but requires field testing.
|
||||
|
||||
**Delivers:**
|
||||
- Speed-adaptive GPS precision (HIGH_ACCURACY >12 km/h, BALANCED <8 km/h with hysteresis)
|
||||
- Lifecycle-aware GPS control (stop when backgrounded without overlay)
|
||||
- Wake lock conditional logic (only when tracking with overlay)
|
||||
- WorkManager background tasks (replaces aggressive timers)
|
||||
- Optimized LocationSettings (intervalDuration, distanceFilter, foregroundNotificationConfig)
|
||||
- Battery usage monitoring integration
|
||||
|
||||
**Addresses Features:**
|
||||
- Speed-adaptive GPS precision (competitive)
|
||||
- WorkManager background tasks (competitive)
|
||||
|
||||
**Avoids Pitfalls:**
|
||||
- Excessive battery drain from GPS (CRITICAL #4)
|
||||
- GPS stream subscription leaks (CRITICAL #2)
|
||||
|
||||
**Research Flags:** MEDIUM RISK — Requires physical device testing for threshold tuning, Android OEM behavior varies (Samsung, Xiaomi have aggressive battery optimization). Test on Android 11-15 range.
|
||||
|
||||
### Phase Ordering Rationale
|
||||
|
||||
**Dependency chain:**
|
||||
1. Phase 1 (Foundation) → Phase 2 (Provider) — Directory structure needed for organized migration
|
||||
2. Phase 2 (Provider) → Phase 3 (Overlay) — Scoped Provider instances require Provider setup
|
||||
3. Phase 2 (Provider) → Phase 4 (GPS) — Lifecycle-aware GPS needs ViewModel hooks
|
||||
4. Phase 3 (Overlay) and Phase 4 (GPS) are independent — Can be done in parallel or swapped
|
||||
|
||||
**Why this grouping:**
|
||||
- Phase 1 is pure organization, minimal behavior change (low risk warm-up)
|
||||
- Phase 2 is highest risk, requires full async audit (do while fresh)
|
||||
- Phase 3 and 4 are independent optimizations (can be prioritized by business need)
|
||||
|
||||
**How this avoids pitfalls:**
|
||||
- Audit async patterns BEFORE Phase 2 migration (prevents dispose crashes)
|
||||
- Define message schema BEFORE Phase 3 refactor (prevents overlay communication breaks)
|
||||
- Conditional timer logic IN Phase 3 (prevents battery drain violations)
|
||||
- Speed-adaptive GPS IN Phase 4 (addresses Google Play compliance deadline March 2026)
|
||||
|
||||
### Research Flags
|
||||
|
||||
**Phases likely needing deeper research:**
|
||||
- **Phase 3 (Overlay Refactor):** flutter_overlay_window 0.5.0 reliability issues documented but solutions vary. May need platform-specific testing, consider flutter_overlay_window_plus fork (July 2025) if communication fails. Test message serialization edge cases, Android version inconsistencies (11+ notification bubbles vs overlays).
|
||||
- **Phase 4 (GPS Optimization):** Optimal speed thresholds need field testing (suggested 8/12 km/h hysteresis may need tuning). Android Fused Location Provider duty-cycling behavior varies by OEM. Research WorkManager setup for background location (Play Store policy nuances).
|
||||
|
||||
**Phases with standard patterns (skip research-phase):**
|
||||
- **Phase 1 (Foundation):** Well-documented Flutter project structure patterns, official guides available
|
||||
- **Phase 2 (Provider Migration):** Established Provider patterns, official Flutter docs cover ChangeNotifier lifecycle
|
||||
|
||||
**Open research questions for phase planning:**
|
||||
1. Is there event-based alternative to overlay status polling? Or accept polling at 5s instead of 1s?
|
||||
2. Should GpsDataManager singleton use reference counting or Provider-managed lifecycle?
|
||||
3. What's cleanest pattern for Provider + Singleton (ChangeNotifierProvider.value vs custom dispose logic)?
|
||||
|
||||
## Confidence Assessment
|
||||
|
||||
| Area | Confidence | Notes |
|
||||
|------|------------|-------|
|
||||
| Stack | HIGH | Existing packages verified with official docs, no new dependencies needed, Provider is official Flutter recommendation |
|
||||
| Features | HIGH | Clear restructure goals from project context, MVVM patterns well-documented for 2026 Flutter |
|
||||
| Architecture | HIGH | Official Flutter architecture guide 2026, layer structure matches single-screen app complexity |
|
||||
| Pitfalls | HIGH | Multiple 2025-2026 sources confirm async lifecycle issues, battery drain enforcement documented by Google |
|
||||
| Overlay Communication | MEDIUM | flutter_overlay_window 0.5.0 issues confirmed on GitHub, but solutions vary — needs testing |
|
||||
| GPS Optimization | MEDIUM | Speed thresholds need field validation, Android OEM behavior varies |
|
||||
|
||||
**Overall confidence:** HIGH for architectural approach, MEDIUM for overlay/GPS implementation details
|
||||
|
||||
### Gaps to Address
|
||||
|
||||
**Research gaps needing validation during implementation:**
|
||||
|
||||
1. **flutter_overlay_window 0.5.0 specific limitations** — Project context mentions "shareData fundamentally broken" but GitHub issues are vague. Need to test overlay→main communication thoroughly during Phase 3. Consider flutter_overlay_window_plus fork (2025) if message passing fails. May need workarounds like accepting 5s polling instead of 1s, or one-way communication only.
|
||||
|
||||
2. **GpsDataManager singleton lifecycle** — Currently nothing calls dispose() on singleton. Need to decide during Phase 2: Reference counting pattern? Provider-managed disposal? App-global persistence? Research needed on cleanest Provider + Singleton pattern in Flutter 2026.
|
||||
|
||||
3. **Optimal GPS accuracy switching thresholds** — Project spec suggests ~10 km/h with 8/12 hysteresis. Need field testing during Phase 4 to validate battery vs responsiveness tradeoff. Android Fused Location Provider behavior varies by OEM (Samsung, Xiaomi, OnePlus have different duty-cycling). Test on multiple devices.
|
||||
|
||||
4. **Overlay status polling alternative** — Current 1s polling is workaround for broken message-based detection. Research during Phase 3: Is there event-based alternative via flutter_overlay_window_plus? Or accept polling at 5s instead of 1s as reasonable compromise?
|
||||
|
||||
5. **Manual testing coverage** — Zero automated tests means regression testing is manual-only. Create comprehensive test checklist during Phase 1. Consider adding basic smoke tests post-restructure to catch future regressions (even minimal widget tests help).
|
||||
|
||||
6. **Android lifecycle edge cases** — Permission revocation while app running, GPS disabled mid-tracking, overlay permission removed externally, duty-cycling on different Android versions. These need dedicated test scenarios during Phases 2-3.
|
||||
|
||||
## Sources
|
||||
|
||||
### Primary (HIGH confidence)
|
||||
- [Flutter Official: Guide to App Architecture](https://docs.flutter.dev/app-architecture/guide) — MVVM pattern, layer separation, 2026 best practices
|
||||
- [Flutter Official: Simple State Management (Provider)](https://docs.flutter.dev/data-and-backend/state-mgmt/simple) — Provider patterns, ChangeNotifier lifecycle
|
||||
- [Android Developers Blog: Excessive Wake Lock Metric (2025)](https://android-developers.googleblog.com/2025/09/guide-to-excessive-wake-lock-usage.html) — Battery drain enforcement, March 2026 deadline, thresholds
|
||||
- [Android Official: Location Request Updates](https://developer.android.com/develop/sensors-and-location/location/request-updates) — LocationSettings API, accuracy mapping
|
||||
- [Android Official: Location Battery Optimization](https://developer.android.com/develop/sensors-and-location/location/battery) — GPS power best practices, duty-cycling
|
||||
- [Geolocator Package Docs](https://pub.dev/packages/geolocator) — LocationSettings usage, Android 14+ compatibility
|
||||
- [Provider Package Docs](https://pub.dev/packages/provider) — Consumer optimization, Selector patterns, disposal
|
||||
|
||||
### Secondary (MEDIUM confidence)
|
||||
- [flutter_overlay_window GitHub Issues](https://github.com/X-SLAYER/flutter_overlay_window/issues) — Known reliability problems, communication failures
|
||||
- [flutter_overlay_window Issue #115](https://github.com/X-SLAYER/flutter_overlay_window/issues/115) — "Can't display data on overlay" matches project bug
|
||||
- [Geolocator Issue #1682](https://github.com/Baseflow/flutter-geolocator/issues/1682) — Stream subscription memory leak
|
||||
- [DCM: Memory Leaks in Dart and Flutter (2024)](https://dcm.dev/blog/2024/10/21/lets-talk-about-memory-leaks-in-dart-and-flutter/) — Disposal patterns, StreamSubscription leaks
|
||||
- [Advanced Location Tracking in Flutter: 2026 Guide](https://medium.com/@ali.mohamed.hgr/advanced-location-tracking-in-flutter-the-complete-2026-guide-cce138f2d558) — GPS architecture patterns
|
||||
- [Best Flutter State Management Libraries 2026](https://foresightmobile.com/blog/best-flutter-state-management) — Provider vs Riverpod comparison
|
||||
- [Flutter Project Structure: Feature-first or Layer-first?](https://codewithandrea.com/articles/flutter-project-structure/) — Directory organization guidance
|
||||
|
||||
### Tertiary (LOW confidence, needs validation)
|
||||
- [flutter_overlay_window_plus](https://pub.dev/packages/flutter_overlay_window_plus) — July 2025 fork claiming fixes, untested
|
||||
- Various Medium articles on Provider migration best practices (2024-2025)
|
||||
- Stack Overflow discussions on setState after dispose patterns
|
||||
- Community blog posts on ChangeNotifier optimization
|
||||
|
||||
---
|
||||
**Research completed:** 2026-02-09
|
||||
**Ready for roadmap:** Yes
|
||||
**Estimated restructure effort:** 52-70 hours (1.5-2 weeks full-time)
|
||||
**Highest risk phases:** Phase 2 (Provider Migration), Phase 3 (Overlay Refactor)
|
||||
Reference in New Issue
Block a user