3c4231d307
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>
709 lines
28 KiB
Markdown
709 lines
28 KiB
Markdown
# 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)
|