diff --git a/.planning/phases/01-foundation/01-RESEARCH.md b/.planning/phases/01-foundation/01-RESEARCH.md new file mode 100644 index 0000000..457bae6 --- /dev/null +++ b/.planning/phases/01-foundation/01-RESEARCH.md @@ -0,0 +1,612 @@ +# Phase 1: Foundation - Research + +**Researched:** 2026-02-09 +**Domain:** Flutter code organization and refactoring patterns +**Confidence:** HIGH + +## Summary + +This research investigates best practices for reorganizing a monolithic Flutter application into a modular, layer-first directory structure. The focus is on extracting immutable data models, centralizing behavioral constants, implementing proper debug logging, and ensuring safe incremental migration without breaking the application. + +The standard approach in Flutter emphasizes separation of concerns with a layer-first structure (screens/, widgets/, services/, providers/, models/, config/) for small to medium applications. Flutter officially recommends the MVVM pattern with clear boundaries between UI and data layers. File organization follows strict Dart conventions: snake_case for files, UpperCamelCase for classes, and lowerCamelCase for constants (though SCREAMING_CAPS remains acceptable for legacy consistency). + +Key recommendations focus on immutable models with copyWith() methods, domain-grouped constants using static classes, debug-only logging with kDebugMode, and incremental migration where every step maintains a runnable application. + +**Primary recommendation:** Use layer-first structure with immutable models, domain-grouped constants in abstract classes, and kDebugMode-guarded logging. Migrate incrementally with immediate import updates to avoid broken states. + +## Standard Stack + +The established libraries/tools for this domain: + +### Core +| Library | Version | Purpose | Why Standard | +|---------|---------|---------|--------------| +| Flutter SDK | 3.38.6 | Cross-platform UI framework | Official framework (as of 2026-01-14) | +| Dart | 3.10.3 | Programming language | Flutter's language (updated 2026-02-05) | +| flutter/foundation.dart | Built-in | Debug mode detection | Provides kDebugMode for conditional logging | + +### Supporting +| Library | Version | Purpose | When to Use | +|---------|---------|---------|-------------| +| freezed | Latest | Code generation for immutable classes | Complex models requiring serialization/equality | +| copy_with_extension | Latest | Generates copyWith methods | When avoiding manual copyWith boilerplate | +| dart_mappable | Latest | JSON serialization with fromMap/toMap | Models persisted to SharedPreferences | + +### Alternatives Considered +| Instead of | Could Use | Tradeoff | +|------------|-----------|----------| +| Manual copyWith | Freezed package | Manual: More control, less boilerplate. Freezed: Auto-generates but adds build step | +| Layer-first | Feature-first | Layer-first better for small/medium apps, feature-first for large multi-feature apps | +| lowerCamelCase constants | SCREAMING_CAPS | lowerCamelCase is modern Dart standard, SCREAMING_CAPS acceptable for legacy consistency | + +**Installation:** +No additional dependencies required for basic refactoring. Optional code generation packages: +```bash +flutter pub add freezed_annotation +flutter pub add --dev freezed build_runner +``` + +## Architecture Patterns + +### Recommended Project Structure +``` +lib/ +├── screens/ # Top-level screen widgets (1:1 with viewmodels) +├── widgets/ # Reusable UI components (flat structure, no subdirectories) +├── providers/ # State management (Provider pattern) +├── services/ # Business logic and external API wrappers +├── models/ # Immutable data classes with copyWith() +├── config/ # Constants grouped by domain +│ ├── gps_constants.dart +│ ├── ui_constants.dart +│ ├── timing_constants.dart +│ └── overlay_constants.dart +└── main.dart # App entry point +``` + +**Key principles:** +- Layer-first organization: Group by technical layer, not feature +- Flat widget directory: All widgets directly in widgets/, no subdirectories +- Overlay code distributed: Overlay widgets in widgets/, overlay services in services/ +- File naming: snake_case matching class names (GpsDataManager → gps_data_manager.dart) + +### Pattern 1: Immutable Models with copyWith() +**What:** Data classes with final fields and a copyWith() method for creating modified copies +**When to use:** All data models, especially those shared between components or persisted +**Example:** +```dart +// Source: Current codebase (lib/services/gps_data_manager.dart) +class ProcessedGpsData { + final double speed; + final double heading; + final String displaySpeed; + final String displayHeading; + final bool isSpeedValid; + final bool isHeadingValid; + + const ProcessedGpsData({ + required this.speed, + required this.heading, + required this.displaySpeed, + required this.displayHeading, + required this.isSpeedValid, + required this.isHeadingValid, + }); + + ProcessedGpsData copyWith({ + double? speed, + double? heading, + String? displaySpeed, + String? displayHeading, + bool? isSpeedValid, + bool? isHeadingValid, + }) { + return ProcessedGpsData( + speed: speed ?? this.speed, + heading: heading ?? this.heading, + displaySpeed: displaySpeed ?? this.displaySpeed, + displayHeading: displayHeading ?? this.displayHeading, + isSpeedValid: isSpeedValid ?? this.isSpeedValid, + isHeadingValid: isHeadingValid ?? this.isHeadingValid, + ); + } +} +``` + +**Limitation:** Standard copyWith cannot distinguish between "not provided" and "set to null". For nullable fields requiring explicit null setting, use code generation (Freezed) or sentinel pattern. + +### Pattern 2: Domain-Grouped Constants with Static Classes +**What:** Constants organized into abstract classes with static members, grouped by functional domain +**When to use:** Behavioral constants (timeouts, thresholds, GPS settings), not UI spacing/padding +**Example:** +```dart +// Source: Flutter community best practices 2026 +// config/gps_constants.dart +abstract class GpsConfig { + static const Duration initialFixTimeout = Duration(seconds: 20); + static const Duration updateTimeout = Duration(seconds: 2); + static const Duration staleDataThreshold = Duration(seconds: 4); + static const double validHeadingMin = 0.0; + static const double validHeadingMax = 360.0; + static const double lowSpeedThreshold = 1.0; +} + +// config/timing_constants.dart +abstract class TimingConfig { + static const Duration heartbeatInterval = Duration(seconds: 5); + static const Duration overlayStatusCheckInterval = Duration(milliseconds: 1000); + static const Duration tapCloseDelay = Duration(milliseconds: 500); +} + +// config/overlay_constants.dart +abstract class OverlayConfig { + static const double widthPercentage = 0.45; + static const double aspectRatio = 0.6; // height = width * aspectRatio + static const double backgroundOpacity = 0.85; + static const double borderOpacity = 0.3; +} +``` + +**Naming convention:** lowerCamelCase (modern Dart standard) or SCREAMING_CAPS (legacy consistency) +**File organization:** One file per domain, descriptive names (gps_constants.dart, not constants.dart) + +### Pattern 3: Theme Model with Internal Palette Storage +**What:** A model that holds all available color palettes internally, exposing only the current palette's colors and a method to cycle themes +**When to use:** Managing multiple theme options with rotation capability +**Example:** +```dart +// Source: Design decision from CONTEXT.md +// models/theme_model.dart +class ThemeModel { + final int _currentIndex; + final List _allPalettes; + + const ThemeModel({ + required int currentIndex, + required List allPalettes, + }) : _currentIndex = currentIndex, _allPalettes = allPalettes; + + // Expose current palette's colors with named accessors + ColorTheme get current => _allPalettes[_currentIndex % _allPalettes.length]; + String get currentSetName => current.name; + + // Method for rotating themes + ThemeModel rotate() { + return ThemeModel( + currentIndex: (_currentIndex + 1) % _allPalettes.length, + allPalettes: _allPalettes, + ); + } + + ThemeModel copyWith({int? currentIndex, List? allPalettes}) { + return ThemeModel( + currentIndex: currentIndex ?? _currentIndex, + allPalettes: allPalettes ?? _allPalettes, + ); + } +} +``` + +**Separation of concerns:** Persistence (SharedPreferences) handled separately, not in the model itself. + +### Pattern 4: Debug-Only Logging with kDebugMode +**What:** Conditional logging that only executes in debug builds using kDebugMode +**When to use:** Replacing all print() statements; all debug logging +**Example:** +```dart +// Source: Flutter official docs - docs.flutter.dev/testing/code-debugging +import 'package:flutter/foundation.dart'; + +class Logger { + static const String _reset = '\x1B[0m'; + static const String _red = '\x1B[31m'; + static const String _yellow = '\x1B[33m'; + static const String _blue = '\x1B[34m'; + static const String _gray = '\x1B[90m'; + + static void error(String message, [String? caller]) { + if (kDebugMode) { + final timestamp = DateTime.now().toIso8601String(); + final callerTag = caller != null ? '[$caller] ' : ''; + debugPrint('$_red[ERROR]$_reset $timestamp $callerTag$message'); + } + } + + static void warn(String message, [String? caller]) { + if (kDebugMode) { + final timestamp = DateTime.now().toIso8601String(); + final callerTag = caller != null ? '[$caller] ' : ''; + debugPrint('$_yellow[WARN]$_reset $timestamp $callerTag$message'); + } + } + + static void info(String message, [String? caller]) { + if (kDebugMode) { + final timestamp = DateTime.now().toIso8601String(); + final callerTag = caller != null ? '[$caller] ' : ''; + debugPrint('$_blue[INFO]$_reset $timestamp $callerTag$message'); + } + } + + static void debug(String message, [String? caller]) { + if (kDebugMode) { + final timestamp = DateTime.now().toIso8601String(); + final callerTag = caller != null ? '[$caller] ' : ''; + debugPrint('$_gray[DEBUG]$_reset $timestamp $callerTag$message'); + } + } +} +``` + +**Key benefits:** +- Tree shaking removes logging code entirely in release builds +- debugPrint() avoids log truncation (unlike print()) +- Consistent format with timestamps and caller identification + +### Pattern 5: Safe Incremental Migration +**What:** Refactoring strategy where app compiles and runs after every change +**When to use:** All code reorganization, especially when moving files between directories +**Example workflow:** +``` +Wave 1: Create directory structure + - mkdir lib/screens lib/widgets lib/providers lib/services lib/models lib/config + - Verify: flutter analyze (no errors) + +Wave 2: Extract first model (ProcessedGpsData) + - Create lib/models/processed_gps_data.dart + - Move ProcessedGpsData class + - Update all imports in consuming files + - Verify: flutter run --debug (app launches successfully) + +Wave 3: Extract second model (OverlayMessage) + - Create lib/models/overlay_message.dart + - Move OverlayMessage class + - Update all imports + - Verify: flutter run --debug + +Wave 4: Extract constants + - Create lib/config/gps_constants.dart + - Extract GPS-related constants + - Update references + - Verify: flutter run --debug +``` + +**Critical rules:** +- No temporary re-exports (update imports immediately) +- No broken intermediate states (every wave is a working checkpoint) +- Test on physical device after each wave (bench phone verification) + +### Anti-Patterns to Avoid + +- **Giant constants file:** Don't create a single constants.dart with all constants. Split by domain (gps_constants.dart, timing_constants.dart, etc.) for maintainability. + +- **Premature abstraction:** Don't extract UI padding/sizing constants. These should remain inline as they're context-specific and extracting them reduces readability. + +- **Broken intermediate states:** Don't move files without immediately updating imports. Every commit should leave the app in a runnable state. + +- **debugPrint() without kDebugMode:** debugPrint() runs in all modes. Always wrap with `if (kDebugMode)` or use a logger utility that does this internally. + +- **SCREAMING_CAPS by default:** Modern Dart prefers lowerCamelCase for constants. Only use SCREAMING_CAPS for consistency with existing code that already uses it. + +- **Feature-first for small apps:** Don't use feature-first structure for small/medium apps. Layer-first is simpler and avoids excessive "jumping" between distant files. + +## Don't Hand-Roll + +Problems that look simple but have existing solutions: + +| Problem | Don't Build | Use Instead | Why | +|---------|-------------|-------------|-----| +| copyWith with nullable fields | Custom sentinel pattern | freezed package | Language limitation: can't distinguish null vs omitted. Freezed handles this correctly. | +| Model serialization | Custom toMap/fromMap | dart_mappable or freezed | Handles nested objects, type safety, null safety edge cases. | +| File moving automation | Manual file moves + find/replace imports | IDE refactoring (VSCode: F2 rename, Move to file) | IDE tracks references and updates imports automatically. | +| Debug vs release logic | Custom build flags or globals | kDebugMode from flutter/foundation.dart | Official constant, enables tree shaking, zero runtime cost. | +| Enum-like constant groups | Individual constants | Dart enums or abstract classes with static consts | Type safety, IDE autocomplete, grouped organization. | + +**Key insight:** Flutter's ecosystem has mature code generation tools (freezed, build_runner, dart_mappable) that handle edge cases better than hand-rolled solutions. For a foundation phase, start simple (manual copyWith, basic models) but plan for code generation if complexity grows. + +## Common Pitfalls + +### Pitfall 1: Import Path Breakage During File Moves +**What goes wrong:** Moving files to new directories without updating imports causes compilation errors across the codebase. +**Why it happens:** Dart uses relative imports (e.g., `import 'speed_units.dart'`) that break when file locations change. Forgetting to update all import statements leaves the app uncompilable. +**How to avoid:** +- Use IDE refactoring tools (VSCode F2 rename, "Move to file" command) which automatically update imports +- If moving manually: Use global search for the old import path before moving the file +- Update all imports immediately in the same commit as the file move +- Run `flutter analyze` after every file move to catch broken imports +**Warning signs:** +- Compilation errors like "Target of URI doesn't exist" +- Analyzer errors showing unresolved imports +- Multiple files showing "undefined name" errors for moved classes + +### Pitfall 2: Constant Naming Convention Confusion +**What goes wrong:** Mixing SCREAMING_CAPS and lowerCamelCase constants inconsistently across the codebase. +**Why it happens:** Older Dart code used SCREAMING_CAPS (like Java), but modern Dart style guide recommends lowerCamelCase. Developers unfamiliar with the style change default to SCREAMING_CAPS. +**How to avoid:** +- Check official Dart style guide (updated 2026-02-05): "use lowerCamelCase for constant variables" +- For this project: CONTEXT.md specifies SCREAMING_CAPS for consistency with hypothetical existing patterns +- Document the chosen convention in a comment at the top of each constants file +- Use a linter rule to enforce consistency: `constant_identifier_names: false` (allows both styles) +**Warning signs:** +- Mix of `GPS_TIMEOUT` and `gpsTimeout` in same file +- Refactoring changing constant names from const to final breaking style consistency + +### Pitfall 3: Extracting UI Constants Prematurely +**What goes wrong:** Moving layout-specific values (padding, font sizes, percentages) to constants files reduces readability without adding value. +**Why it happens:** Over-zealous adherence to "no magic numbers" principle without considering context. +**How to avoid:** +- Only extract behavioral constants (timeouts, thresholds, business logic values) +- Leave UI values inline when they're context-specific (e.g., `fontSize * 0.5` for subfont) +- Extract UI constants only when the same value is used in 3+ places OR represents a design system token +- CONTEXT.md explicitly states: "Extract behavioral constants only — leave UI padding/sizing as inline values" +**Warning signs:** +- Constants file with entries like `buttonPadding8`, `fontSize12`, `opacity50` +- Constants used in only one location +- Developers needing to jump between files to understand layout math + +### Pitfall 4: copyWith() Null Ambiguity +**What goes wrong:** Using `model.copyWith(field: null)` doesn't set the field to null; it keeps the existing value due to `??` operator. +**Why it happens:** Dart can't distinguish between omitted parameters and parameters explicitly set to null. The pattern `field ?? this.field` treats both identically. +**How to avoid:** +- For simple models with non-nullable fields: Standard copyWith is sufficient +- For models with nullable fields needing explicit null setting: Use freezed package or sentinel pattern +- Document the limitation in code comments if using manual copyWith +- For this project: ProcessedGpsData has non-nullable fields, so standard copyWith is safe +**Warning signs:** +- Attempting to clear a nullable field: `data.copyWith(optionalField: null)` doesn't work +- Unit tests failing when trying to nullify fields +- Bug reports about "can't reset field to empty state" + +### Pitfall 5: Forgetting kDebugMode for debugPrint() +**What goes wrong:** Assuming debugPrint() is automatically debug-only; it actually runs in all build modes. +**Why it happens:** The name "debugPrint" is misleading — it only prevents log truncation, not production execution. +**How to avoid:** +- Always wrap debugPrint() calls with `if (kDebugMode)` guard +- Create a Logger utility class that internally uses kDebugMode +- Replace all bare print() and debugPrint() calls during refactoring +- Configure linter to warn on bare print() usage: `avoid_print: true` +**Warning signs:** +- Performance impact in release builds from excessive logging +- Logs appearing in production app (if connected to debugger) +- Increased APK size from unremoved log strings (minor but measurable) + +### Pitfall 6: Broken Intermediate States in Migration +**What goes wrong:** Moving multiple files simultaneously, planning to "fix imports later", leaving app uncompilable for hours or days. +**Why it happens:** Attempting to do too much in one commit; underestimating import dependency complexity. +**How to avoid:** +- Adopt wave-based migration: One file or one domain at a time +- After each file move, immediately update all imports +- Run `flutter run --debug` after each wave to verify app launches +- CONTEXT.md requirement: "App must be runnable on a bench phone after each session/wave" +- Commit after each successful wave, not at the end of a large batch +**Warning signs:** +- Git diff showing 20+ files changed +- Compilation errors blocking any testing +- Comments like "TODO: fix imports" in commit messages +- Unable to demonstrate progress on physical device + +### Pitfall 7: Circular Dependency Creation +**What goes wrong:** After reorganization, models depend on services which depend on models, causing compilation errors. +**Why it happens:** Improper separation of concerns; business logic leaking into models; models importing service implementations. +**How to avoid:** +- Maintain dependency direction: Services → Models → Config (never circular) +- Models should be pure data classes with no service imports +- If a model needs formatting logic, use helper methods in services or extensions +- Use dependency injection for services needing other services +**Warning signs:** +- Compilation error: "Circular dependency detected" +- Model files importing from services/ directory +- Config files importing from models/ (should be pure constants) + +## Code Examples + +Verified patterns from official sources and current codebase: + +### File Naming Convention +```dart +// Source: dart.dev/effective-dart/style (updated 2026-02-05) +// Class name: GpsDataManager +// File name: gps_data_manager.dart (lowercase_with_underscores) + +// Class name: ProcessedGpsData +// File name: processed_gps_data.dart + +// Class name: OverlayMessage +// File name: overlay_message.dart +``` + +### Model with Serialization for SharedPreferences +```dart +// Source: Flutter community pattern for SharedPreferences persistence +// models/overlay_message.dart +import 'dart:convert'; + +class OverlayMessage { + final double speed; + final double heading; + final String displaySpeed; + final String displayHeading; + final String themeName; + final String unit; + + const OverlayMessage({ + required this.speed, + required this.heading, + required this.displaySpeed, + required this.displayHeading, + required this.themeName, + required this.unit, + }); + + // For SharedPreferences persistence + Map toMap() { + return { + 'speed': speed, + 'heading': heading, + 'displaySpeed': displaySpeed, + 'displayHeading': displayHeading, + 'themeName': themeName, + 'unit': unit, + }; + } + + factory OverlayMessage.fromMap(Map map) { + return OverlayMessage( + speed: map['speed'] as double, + heading: map['heading'] as double, + displaySpeed: map['displaySpeed'] as String, + displayHeading: map['displayHeading'] as String, + themeName: map['themeName'] as String, + unit: map['unit'] as String, + ); + } + + String toJson() => json.encode(toMap()); + + factory OverlayMessage.fromJson(String source) => + OverlayMessage.fromMap(json.decode(source) as Map); + + OverlayMessage copyWith({ + double? speed, + double? heading, + String? displaySpeed, + String? displayHeading, + String? themeName, + String? unit, + }) { + return OverlayMessage( + speed: speed ?? this.speed, + heading: heading ?? this.heading, + displaySpeed: displaySpeed ?? this.displaySpeed, + displayHeading: displayHeading ?? this.displayHeading, + themeName: themeName ?? this.themeName, + unit: unit ?? this.unit, + ); + } +} +``` + +### Enum-based Model (SpeedUnit pattern) +```dart +// Source: Current codebase lib/speed_units.dart +// models/speed_unit.dart +enum SpeedUnit { + kmh('km/h', 1.0), + mph('mph', 0.621371), + knots('kts', 0.539957); + + const SpeedUnit(this.label, this.multiplier); + + final String label; + final double multiplier; + + double convert(double speedMps) { + return speedMps * 3.6 * multiplier; + } + + SpeedUnit get next { + final values = SpeedUnit.values; + final currentIndex = values.indexOf(this); + return values[(currentIndex + 1) % values.length]; + } +} +``` + +**Note:** This enum pattern is idiomatic Dart and should remain as-is, not converted to a class. Move to models/ directory without structural changes. + +### Directory Structure Creation +```bash +# Source: Planning decision +# Creates all necessary directories in one go +cd lib +mkdir -p screens widgets providers services models config +``` + +### Import Update Pattern +```dart +// Source: Migration best practice +// Before (in main.dart): +import 'speed_units.dart'; +import 'color_themes.dart'; +import 'services/gps_data_manager.dart'; + +// After reorganization: +import 'models/speed_unit.dart'; +import 'models/theme_model.dart'; +import 'services/gps_data_manager.dart'; +import 'screens/speedometer_screen.dart'; +import 'screens/overlay_screen.dart'; +``` + +## State of the Art + +| Old Approach | Current Approach | When Changed | Impact | +|--------------|------------------|--------------|--------| +| SCREAMING_CAPS for constants | lowerCamelCase for constants | Dart style guide update (pre-2025) | SCREAMING_CAPS still acceptable for legacy; new code prefers lowerCamelCase | +| print() for debugging | kDebugMode + debugPrint() | Flutter 2.x+ | Tree shaking removes debug code; no runtime cost in release | +| Manual copyWith for nullable fields | freezed or sentinel pattern | Community evolution 2024-2025 | Standard copyWith can't set fields to null explicitly | +| Feature-first always recommended | Layer-first for small/medium apps | Flutter community 2025 | Official guidance: choose based on project size/complexity | +| Manual JSON serialization | Code generation (freezed, json_serializable) | Flutter 3.x era | Code generation reduces bugs, handles edge cases | + +**Deprecated/outdated:** +- Using plain `print()` without kDebugMode guard: Bloats release builds with debug strings +- Single monolithic constants.dart: Hard to maintain; modern practice uses domain-split files +- Feature-first for all projects: Overkill for apps without many distinct features + +## Open Questions + +Things that couldn't be fully resolved: + +1. **Theme model persistence strategy** + - What we know: CONTEXT.md specifies "Persistence (SharedPreferences) handled separately from the theme model itself" + - What's unclear: Should persistence logic live in a service, provider, or repository pattern? + - Recommendation: Create a ThemeService in services/ that handles loading/saving theme index to SharedPreferences, keeping the ThemeModel pure + +2. **Exact fields for OverlayMessage model** + - What we know: Need to replace unvalidated Map data for overlay communication + - What's unclear: Current overlay messaging uses dynamic Maps; exact fields require examining overlay communication code + - Recommendation: Analyze overlay message content during execution wave to determine required fields (likely: speed, heading, theme, unit) + +3. **Whether to include toMap/fromMap in all models** + - What we know: ProcessedGpsData is in-memory only; OverlayMessage likely needs serialization for inter-process communication; ThemeModel index needs persistence + - What's unclear: Best practice for mixed persistence needs within models/ directory + - Recommendation: Add toMap/fromMap only to models that need it (OverlayMessage). For ProcessedGpsData, skip serialization (in-memory only). Document this decision in model file comments. + +4. **Logger implementation details** + - What we know: Need 4 levels (error, warn, info, debug) with caller name and timestamp + - What's unclear: Should logger support automatic caller detection via StackTrace, or require manual caller strings? Should it use ANSI colors for console output? + - Recommendation: Start simple with manual caller strings (`Logger.info('message', 'GpsManager')`). ANSI colors improve readability in terminal — include them. Can enhance with automatic caller detection later if needed. + +## Sources + +### Primary (HIGH confidence) +- [Flutter App Architecture Guide](https://docs.flutter.dev/app-architecture/guide) - Official MVVM architecture recommendations (updated 2026-01-14) +- [Dart Effective Style Guide](https://dart.dev/effective-dart/style) - Official naming conventions (updated 2026-02-05) +- [Flutter Debug Documentation](https://docs.flutter.dev/testing/code-debugging) - kDebugMode and debugPrint() usage +- Current codebase analysis - Existing patterns in lib/services/gps_data_manager.dart, lib/speed_units.dart, lib/color_themes.dart + +### Secondary (MEDIUM confidence) +- [Flutter Project Structure: Feature-first or Layer-first?](https://codewithandrea.com/articles/flutter-project-structure/) - Community best practices +- [Managing Flutter Logs](https://medium.com/@punithsuppar7795/managing-flutter-logs-reducing-noise-in-debug-console-0229ff7a9235) - kDebugMode patterns +- [Flutter Constants Best Practices](https://dev.to/gulsenkeskin/flutter-constants-best-practices-55og) - Constants organization patterns +- [Understanding copyWith in Flutter](https://medium.com/@alaxhenry0121/understanding-copywith-in-flutter-models-a-complete-guide-for-efficient-state-management-4a41f71eea42) - Immutability patterns +- [How to Explicitly Set Null in CopyWith](https://happy-makadiya.medium.com/how-to-explicitly-set-null-in-copywith-parameters-in-dart-a01f9591c3ea) - copyWith() limitations + +### Tertiary (LOW confidence) +- Multiple Medium articles on Flutter architecture (2025-2026) - Community opinions, not official guidance +- Stack Overflow discussions on import management - Practical solutions but not authoritative + +## Metadata + +**Confidence breakdown:** +- Standard stack: HIGH - Official Flutter/Dart documentation from 2026, current codebase patterns verified +- Architecture: HIGH - Official Flutter architecture guide, clear MVVM recommendations, verified layer-first for small apps +- Pitfalls: MEDIUM to HIGH - Mix of official docs (import errors, kDebugMode), community consensus (constants organization), and logical inference (circular dependencies) + +**Research date:** 2026-02-09 +**Valid until:** 2026-03-11 (30 days) - Flutter is in mature phase with stable patterns; architecture recommendations unlikely to change significantly + +**Notes:** +- User decisions in CONTEXT.md constrain research: SCREAMING_CAPS constants (vs Dart standard lowerCamelCase), layer-first structure (confirmed as appropriate), specific theme model design +- No new dependencies specified in prior decisions; research focused on Flutter built-ins and optional code generation +- Current codebase already demonstrates good patterns: immutable ProcessedGpsData with copyWith, enum-based SpeedUnit, singleton GpsDataManager