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:
@@ -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)
|
||||
Reference in New Issue
Block a user