Files
gps-speedo/.planning/codebase/ARCHITECTURE.md
T
Mikkeli Matlock a686e6b0fb docs: map existing codebase
- 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
2026-02-09 18:51:11 +09:00

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 (SpeedometerScreen and OverlaySpeedometer) 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_handler packages
  • 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: GpsDataManager singleton, ProcessedGpsData model, stream subscription and timing logic
  • Depends on: GpsService, SpeedUnit
  • Used by: SpeedometerScreen and OverlaySpeedometer

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: SpeedUnit enum, ColorTheme classes, theme collection
  • Depends on: Flutter Material Design
  • Used by: All UI and data layers

Data Flow

GPS Data Collection & Broadcasting:

  1. main.dart calls GpsDataManager.instance.initialize() on app startup
  2. GpsDataManager requests permissions via GpsService.requestPermissions()
  3. GpsDataManager calls Geolocator.getCurrentPosition() with 20s timeout for initial fix
  4. Once initial position obtained, GpsDataManager subscribes to GpsService.positionStream() with 2s update timeout
  5. Raw Position updates trigger GpsDataManager._onPositionUpdate()
  6. GpsDataManager validates speed/heading, formats display strings, creates ProcessedGpsData
  7. GpsDataManager broadcasts via _dataController.add(newData) (broadcast stream)
  8. Both SpeedometerScreen and OverlaySpeedometer receive updates via stream subscriptions

Main App Display Update:

  1. SpeedometerScreen._onGpsDataUpdate() receives ProcessedGpsData from stream
  2. Updates local state: _currentGpsData, _currentUnit, _currentThemeIndex
  3. Rebuilds UI with converted speed value and formatted heading
  4. If overlay is active, calls _pushDataToOverlay() to sync overlay display

Overlay Activation & Communication:

  1. User taps compass area of main app
  2. _showFloatingWindow() executes: prepares display data, calls FlutterOverlayWindow.shareData(), launches overlay
  3. FlutterOverlayWindow.showOverlay() creates new process running OverlaySpeedometer
  4. Overlay's _listenToMainAppMessages() sets up listener for overlayListener
  5. Main app pushes updates via periodic _pushDataToOverlay() calls when overlay active
  6. Overlay applies updates to state and rebuilds display

Background Persistence:

  1. App detects background transition via didChangeAppLifecycleState()
  2. Enables wake lock via WakelockPlus.enable()
  3. If overlay active, starts heartbeat timer every 5 seconds
  4. Heartbeat calls _pushDataToOverlay() to maintain communication with overlay process
  5. 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 run or app launch
  • Responsibilities: Create SpeedometerApp, initialize MaterialApp theme, set home to SpeedometerScreen

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: ProcessedGpsData copied with isSpeedValid=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 _errorMessage for 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 checks kDebugMode
  • 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() from permission_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.6 for 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