a686e6b0fb
- STACK.md - Technologies and dependencies - ARCHITECTURE.md - System design and patterns - STRUCTURE.md - Directory layout - CONVENTIONS.md - Code style and patterns - TESTING.md - Test structure - INTEGRATIONS.md - External services - CONCERNS.md - Technical debt and issues
9.5 KiB
9.5 KiB
Architecture
Analysis Date: 2026-02-09
Pattern Overview
Overall: Layered Service Pattern with Reactive Data Flow
Key Characteristics:
- Singleton Service Layer - Centralized GPS data management via
GpsDataManager - Reactive Streams - Broadcast streams for reactive UI updates across main app and overlay
- Direct Communication - Main app and floating window communicate via
FlutterOverlayWindow.shareData() - Stateful Widget Hierarchy - Two independent stateful widgets (
SpeedometerScreenandOverlaySpeedometer) consuming shared GPS data - Widget-to-Widget Data Push - Main app explicitly pushes formatted data to overlay instead of shared state management
Layers
GPS Hardware Layer:
- Purpose: Acquire raw GPS data from device sensors
- Location:
lib/gps_service.dart - Contains: GPS permission handling, position stream creation, heading calculations
- Depends on:
geolocator,permission_handlerpackages - Used by:
GpsDataManager
GPS Data Management Layer:
- Purpose: Process raw GPS data, validate signals, manage stale data timeouts, broadcast processed data
- Location:
lib/services/gps_data_manager.dart - Contains:
GpsDataManagersingleton,ProcessedGpsDatamodel, stream subscription and timing logic - Depends on:
GpsService,SpeedUnit - Used by:
SpeedometerScreenandOverlaySpeedometer
UI State & Rendering Layer:
- Purpose: Consume GPS data, manage theme/unit selection, render main app and overlay screens
- Location:
lib/main.dart - Contains:
SpeedometerScreen(main app widget),OverlaySpeedometer(overlay widget), layout builders, gesture handlers - Depends on:
GpsDataManager,ColorThemes,SpeedUnit,WakelockPlus,FlutterOverlayWindow,BgLauncher - Used by: Flutter framework
Configuration/Utilities Layer:
- Purpose: Provide constants and helper functions for speed units, color themes
- Location:
lib/speed_units.dart,lib/color_themes.dart - Contains:
SpeedUnitenum,ColorThemeclasses, theme collection - Depends on: Flutter Material Design
- Used by: All UI and data layers
Data Flow
GPS Data Collection & Broadcasting:
main.dartcallsGpsDataManager.instance.initialize()on app startupGpsDataManagerrequests permissions viaGpsService.requestPermissions()GpsDataManagercallsGeolocator.getCurrentPosition()with 20s timeout for initial fix- Once initial position obtained,
GpsDataManagersubscribes toGpsService.positionStream()with 2s update timeout - Raw
Positionupdates triggerGpsDataManager._onPositionUpdate() GpsDataManagervalidates speed/heading, formats display strings, createsProcessedGpsDataGpsDataManagerbroadcasts via_dataController.add(newData)(broadcast stream)- Both
SpeedometerScreenandOverlaySpeedometerreceive updates via stream subscriptions
Main App Display Update:
SpeedometerScreen._onGpsDataUpdate()receivesProcessedGpsDatafrom stream- Updates local state:
_currentGpsData,_currentUnit,_currentThemeIndex - Rebuilds UI with converted speed value and formatted heading
- If overlay is active, calls
_pushDataToOverlay()to sync overlay display
Overlay Activation & Communication:
- User taps compass area of main app
_showFloatingWindow()executes: prepares display data, callsFlutterOverlayWindow.shareData(), launches overlayFlutterOverlayWindow.showOverlay()creates new process runningOverlaySpeedometer- Overlay's
_listenToMainAppMessages()sets up listener foroverlayListener - Main app pushes updates via periodic
_pushDataToOverlay()calls when overlay active - Overlay applies updates to state and rebuilds display
Background Persistence:
- App detects background transition via
didChangeAppLifecycleState() - Enables wake lock via
WakelockPlus.enable() - If overlay active, starts heartbeat timer every 5 seconds
- Heartbeat calls
_pushDataToOverlay()to maintain communication with overlay process - On resume, disables heartbeat
State Management:
- No centralized Provider - App uses direct state via
setState()in main screen - No local persistence - GPS data is ephemeral (speed, heading, validity flags)
- Settings stored in SharedPreferences - Would be implemented here if settings persistence added
- Overlay state isolated - Overlay maintains its own independent state, receives push updates from main app
Key Abstractions
ProcessedGpsData (Model):
- Purpose: Represents validated, formatted GPS information
- Examples:
lib/services/gps_data_manager.dart(lines 6-40) - Pattern: Immutable data class with
copyWith()constructor for updates - Contains:
speed(m/s),heading(degrees), display strings, validity flags
GpsDataManager (Singleton Service):
- Purpose: Centralized GPS lifecycle and data processing
- Examples:
lib/services/gps_data_manager.dart(lines 42-263) - Pattern: Singleton (
_instance ??= GpsDataManager._internal()) with public stream and synchronous getter - Manages: Permission handling, GPS subscription, stale data timeouts, format conversion
SpeedUnit (Configuration Enum):
- Purpose: Speed unit definitions with conversion logic
- Examples:
lib/speed_units.dart(lines 1-20) - Pattern: Enum with associated data (label, multiplier) and methods (
convert(),next) - Values:
kmh(1.0x),mph(0.621371x),knots(0.539957x) all convert from m/s via 3.6 multiplier
ColorTheme (Configuration):
- Purpose: Immutable theme definition for UI colors
- Examples:
lib/color_themes.dart(lines 3-19) - Pattern: Immutable data class with predefined theme collection
- Used by: Both main app and overlay for consistent visual styling
OverlayListener Pattern:
- Purpose: Async message passing from main app to overlay process
- Method:
FlutterOverlayWindow.overlayListener.listen()(broadcast stream from plugin) - Data format: Maps with action key and payload (speed, heading, theme index, etc.)
Entry Points
Main App Entry:
- Location:
lib/main.dart(lines 22-24) - Triggers:
flutter runor app launch - Responsibilities: Create
SpeedometerApp, initialize MaterialApp theme, set home toSpeedometerScreen
Overlay Entry:
- Location:
lib/main.dart(lines 27-34:overlayMain()) - Triggers:
FlutterOverlayWindow.showOverlay()with overlay file parameter - Responsibilities: Create isolated MaterialApp, set home to
OverlaySpeedometer, no theme inheritance from main app - Annotation:
@pragma("vm:entry-point")required for Flutter to recognize as valid entry point
Speedometer Screen (Main):
- Location:
lib/main.dart(lines 56-837:SpeedometerScreen,_SpeedometerScreenState) - Triggers: Set as home widget in MaterialApp
- Responsibilities: Initialize GPS manager, subscribe to data stream, render portrait/landscape layouts, handle overlay lifecycle, manage background persistence
Overlay Speedometer (Floating Window):
- Location:
lib/main.dart(lines 839-1075:OverlaySpeedometer,_OverlaySpeedometerState) - Triggers: Launched via
FlutterOverlayWindow.showOverlay() - Responsibilities: Listen for main app updates, render speed/heading display, handle close and long-press gestures, send close signal to main app
Error Handling
Strategy: Graceful degradation with user-visible status
Patterns:
- GPS Service Disabled: Set display text to "GPS OFF", mark validity flags false, continue app without crash
- Permissions Denied: Set display to "NO PERM", mark validity flags false, allow user to retry via OS settings prompt
- GPS Timeout:
ProcessedGpsDatacopied withisSpeedValid=false, display text set to "--", heading may remain valid if recently received - Stale Data: Timer-based: after 4 seconds without update, set speed to "--" but keep heading if valid
- Overlay Errors: Wrapped in try-catch in
_showFloatingWindow()and_closeFloatingWindow(), silent failures prevent main app crash - Stream Errors: Caught in listener error handler, sets
_errorMessagefor display, allows user to see issue
Cross-Cutting Concerns
Logging:
- Direct
print()statements with emoji prefix codes for identification - Debug-only printing via
customDebugPrint()which checkskDebugMode - Format:
[Component] emoji Descriptive message - Examples:
[GpsDataManager] 📡 Raw GPS update:,[Main] ❌ GPS stream error callback:
Validation:
- Speed validation:
position.speed.isFinite && position.speed >= 0 - Heading validation:
position.heading.isFinite && position.heading >= 0 && position.heading < 360 - Invalid data triggers fallback: speed becomes 0.0, heading keeps last valid value
- Display text fallback: "--" used for invalid/stale speed data
Permissions:
- Requested via
Permission.locationWhenInUse.request()frompermission_handler - Overlay overlay permissions checked via
FlutterOverlayWindow.isPermissionGranted() - No permission flow UI; user must grant in Android system settings
Background Lifecycle:
- Wake lock enabled on init via
WakelockPlus.enable() - Lifecycle observer via
WidgetsBinding.instance.addObserver(this) - Background heartbeat timer prevents process hibernation when overlay active
- Multiple timer cleanup in dispose to prevent memory leaks
Overlay Synchronization:
- Size calculated in main app:
(physicalSize.width * 0.45).round()for width,width * 0.6for height - Size sent to overlay via
shareData()on creation - Theme index and unit index sent on every update
- Speed value re-converted for overlay to use (overlay doesn't have access to
_currentUnit)
Architecture analysis: 2026-02-09