refactor(data): create layered architecture with core/data/ui packages
- Move constants from lib/config/ to lib/core/constants/ - Move color themes to lib/core/theme/ - Create LocationData domain model in data/models/ - Create lightweight GpsService in data/services/ - Create LocationRepository in data/repositories/ consolidating GpsDataManager business logic (GPS stream, staleness, precision) - Create shared SpeedDisplay widget in ui/shared/ - Defer overlay feature — stubs reserved in ui/features/overlay/
This commit is contained in:
@@ -0,0 +1,81 @@
|
||||
import 'dart:async';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import '../../../core/constants/speed_unit.dart';
|
||||
import '../../../data/models/location_data.dart';
|
||||
import '../../../data/repositories/location_repository.dart';
|
||||
|
||||
/// ViewModel for the speedometer UI.
|
||||
///
|
||||
/// Thin state manager — subscribes to [LocationRepository] and exposes
|
||||
/// the current display state to the view. No data-layer logic here.
|
||||
class SpeedometerViewModel extends ChangeNotifier {
|
||||
final LocationRepository _repository;
|
||||
final SharedPreferences _prefs;
|
||||
|
||||
SpeedUnit _displayUnit = SpeedUnit.kmh;
|
||||
LocationData _displayData = LocationData(
|
||||
speed: 0.0,
|
||||
heading: -1.0,
|
||||
displaySpeed: '--',
|
||||
displayHeading: 'N/A',
|
||||
isSpeedValid: false,
|
||||
isHeadingValid: false,
|
||||
timestamp: DateTime.now(),
|
||||
);
|
||||
|
||||
LocationData get displayData => _displayData;
|
||||
SpeedUnit get displayUnit => _displayUnit;
|
||||
|
||||
SpeedometerViewModel({
|
||||
required LocationRepository repository,
|
||||
required SharedPreferences prefs,
|
||||
}) : _repository = repository,
|
||||
_prefs = prefs;
|
||||
|
||||
void init() {
|
||||
_repository.locationStream.listen(_onLocationData);
|
||||
_loadSettings();
|
||||
}
|
||||
|
||||
void _onLocationData(LocationData data) {
|
||||
_displayData = data;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> _loadSettings() async {
|
||||
_displayUnit = _parseSpeedUnit(_prefs.getString('speed_unit') ?? 'km/h');
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
static SpeedUnit _parseSpeedUnit(String unit) {
|
||||
switch (unit) {
|
||||
case 'mph':
|
||||
return SpeedUnit.mph;
|
||||
case 'kts':
|
||||
return SpeedUnit.knots;
|
||||
default:
|
||||
return SpeedUnit.kmh;
|
||||
}
|
||||
}
|
||||
|
||||
double get displaySpeed {
|
||||
return _displayUnit.convert(_displayData.speed);
|
||||
}
|
||||
|
||||
String get displaySpeedText {
|
||||
return _displayData.displaySpeed;
|
||||
}
|
||||
|
||||
String get displayHeading {
|
||||
return _displayData.displayHeading;
|
||||
}
|
||||
|
||||
bool get isSpeedValid {
|
||||
return _displayData.isSpeedValid;
|
||||
}
|
||||
|
||||
bool get isHeadingValid {
|
||||
return _displayData.isHeadingValid;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import '../../../core/constants/overlay_constants.dart';
|
||||
import '../../../core/theme/color_themes.dart';
|
||||
import '../../providers/settings_provider.dart';
|
||||
|
||||
/// Shared speed display widget — integral + decimal parts with
|
||||
/// staleness dimming and tap-to-cycle-theme.
|
||||
///
|
||||
/// Dumb widget — accepts data as parameters so it works in both
|
||||
/// the old Provider-based and new ViewModel-based architectures.
|
||||
class SpeedDisplay extends StatelessWidget {
|
||||
final double fontSize;
|
||||
final double opacity;
|
||||
final String speedText;
|
||||
final DateTime timestamp;
|
||||
|
||||
const SpeedDisplay({
|
||||
super.key,
|
||||
required this.fontSize,
|
||||
required this.opacity,
|
||||
required this.speedText,
|
||||
required this.timestamp,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Selector<SettingsProvider, ColorTheme>(
|
||||
selector: (_, s) => s.currentTheme,
|
||||
builder: (context, theme, _) {
|
||||
// Staleness — UI-layer concern
|
||||
final age = DateTime.now().difference(timestamp);
|
||||
final showDashes = age >= OverlayConfig.STALENESS_DASH_THRESHOLD;
|
||||
final isDimmed = age >= OverlayConfig.STALENESS_DIM_THRESHOLD;
|
||||
final staleOpacity = isDimmed ? OverlayConfig.STALENESS_DIM_OPACITY : 1.0;
|
||||
final effectiveOpacity = opacity * staleOpacity;
|
||||
|
||||
final displayText = showDashes ? '--' : speedText;
|
||||
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
HapticFeedback.lightImpact();
|
||||
context.read<SettingsProvider>().cycleTheme();
|
||||
},
|
||||
child: _buildSpeedText(displayText, theme, fontSize, effectiveOpacity),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSpeedText(String speedText, ColorTheme theme, double fontSize, double opacity) {
|
||||
if (speedText == '--') {
|
||||
return Text(
|
||||
speedText,
|
||||
style: TextStyle(
|
||||
fontSize: fontSize,
|
||||
fontWeight: FontWeight.w300,
|
||||
color: theme.speedText.withValues(alpha: opacity),
|
||||
fontFamily: 'DIN1451Alt',
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
final parts = speedText.split('.');
|
||||
final integralPart = parts[0];
|
||||
final decimalPart = parts.length > 1 ? '.${parts[1]}' : '.0';
|
||||
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.baseline,
|
||||
textBaseline: TextBaseline.alphabetic,
|
||||
children: [
|
||||
Text(
|
||||
integralPart,
|
||||
style: TextStyle(
|
||||
fontSize: fontSize,
|
||||
fontWeight: FontWeight.w300,
|
||||
color: theme.speedText.withValues(alpha: opacity),
|
||||
fontFamily: 'DIN1451Alt',
|
||||
),
|
||||
),
|
||||
Text(
|
||||
decimalPart,
|
||||
style: TextStyle(
|
||||
fontSize: fontSize * 0.5,
|
||||
fontWeight: FontWeight.w300,
|
||||
color: theme.speedTextSub.withValues(alpha: opacity),
|
||||
fontFamily: 'DIN1451Alt',
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user