From 726a0ce6a88ab9166dcaf66984d22d86a43b201a Mon Sep 17 00:00:00 2001 From: cafca Date: Sat, 25 Apr 2026 15:24:53 +0200 Subject: [PATCH] chore(mobile,plugin): adopt very_good_analysis lint preset MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace flutter_lints with very_good_analysis across mobile app, ferrostar_flutter plugin, and example. Plugin treated as a published library (public_member_api_docs enforced); mobile and example disable docs lint and lines_longer_than_80_chars (dart format is authoritative). Fixes 0 → 0 issues across all three packages. ~509 lints fixed: - 454 auto-fixed by dart fix --apply in mobile - 55 auto-fixed in plugin - ~219 manual fixes in mobile (Dio type args, unawaited/discarded futures, on Object catches, cascade rewrites, dynamic casts, etc.) - ~95 manual fixes in plugin + example (full public API doc comments, pubspec sorting, escaped html in doc comments, line breaks) All tests pass: 177/177 mobile, 21/21 plugin, 1/1 example. --- mobile/analysis_options.yaml | 36 +-- mobile/integration_test/brush_smoke_test.dart | 14 +- .../navigation_smoke_test.dart | 6 +- mobile/lib/api/auth_api.dart | 15 +- mobile/lib/api/client.dart | 5 +- mobile/lib/api/geocode_api.dart | 11 +- mobile/lib/api/locations_api.dart | 9 +- mobile/lib/api/ratings_api.dart | 7 +- mobile/lib/api/ratings_paint_api.dart | 13 +- mobile/lib/api/routing_api.dart | 7 +- mobile/lib/app.dart | 17 +- mobile/lib/config/app_config.dart | 58 ++-- mobile/lib/main.dart | 18 +- mobile/lib/models/paint_response.dart | 2 +- mobile/lib/models/route_state.dart | 5 +- mobile/lib/models/user.dart | 5 +- mobile/lib/navigation/camera_controller.dart | 2 +- mobile/lib/navigation/nav_constants.dart | 4 +- mobile/lib/navigation/navigation_service.dart | 28 +- mobile/lib/providers/auth_provider.dart | 7 +- mobile/lib/providers/brush_provider.dart | 22 +- mobile/lib/providers/home_eta_provider.dart | 11 +- mobile/lib/providers/locale_provider.dart | 3 +- mobile/lib/providers/location_provider.dart | 9 +- .../providers/navigation_camera_provider.dart | 5 +- mobile/lib/providers/navigation_provider.dart | 53 ++-- mobile/lib/providers/onboarding_provider.dart | 3 +- .../providers/rating_overlay_provider.dart | 35 +-- mobile/lib/providers/route_provider.dart | 33 +-- .../providers/search_history_provider.dart | 3 +- mobile/lib/screens/legal_document_screen.dart | 47 ++-- mobile/lib/screens/login_screen.dart | 5 +- mobile/lib/screens/map_screen.dart | 252 +++++++++--------- mobile/lib/screens/onboarding_screen.dart | 47 ++-- mobile/lib/screens/privacy_policy_screen.dart | 7 +- mobile/lib/screens/register_screen.dart | 5 +- mobile/lib/screens/search_screen.dart | 30 +-- mobile/lib/screens/settings_screen.dart | 49 ++-- mobile/lib/services/brush_geometry.dart | 7 +- mobile/lib/services/brush_overlay.dart | 18 +- mobile/lib/services/error_reporter.dart | 10 +- mobile/lib/services/haptics.dart | 2 +- mobile/lib/services/home_marker_service.dart | 19 +- mobile/lib/services/map_style_loader.dart | 3 +- mobile/lib/services/rating_events_client.dart | 13 +- mobile/lib/services/rating_overlay.dart | 17 +- mobile/lib/services/route_drawing.dart | 19 +- mobile/lib/theme/app_theme.dart | 4 +- mobile/lib/theme/typography.dart | 5 +- mobile/lib/widgets/arrived_sheet.dart | 9 +- mobile/lib/widgets/brush_fab.dart | 14 +- mobile/lib/widgets/eta_sheet.dart | 25 +- mobile/lib/widgets/home_sheet.dart | 62 ++--- mobile/lib/widgets/language_picker.dart | 11 +- mobile/lib/widgets/login_form.dart | 5 +- .../lib/widgets/map/compass_fab_inline.dart | 10 +- .../lib/widgets/map/home_sheet_container.dart | 14 +- mobile/lib/widgets/map/nav_top_bar.dart | 30 +-- mobile/lib/widgets/map/navigation_sheet.dart | 16 +- .../lib/widgets/map/recenter_circle_fab.dart | 12 +- mobile/lib/widgets/map/route_sheet.dart | 26 +- mobile/lib/widgets/onboarding_dots.dart | 5 +- mobile/lib/widgets/onboarding_page.dart | 10 +- mobile/lib/widgets/paint_roller_icon.dart | 22 +- mobile/lib/widgets/paint_sheet.dart | 9 +- mobile/lib/widgets/recenter_fab.dart | 5 +- mobile/lib/widgets/register_form.dart | 5 +- mobile/lib/widgets/rerouting_toast.dart | 7 +- mobile/lib/widgets/route_card.dart | 47 ++-- mobile/lib/widgets/route_summary.dart | 14 +- mobile/lib/widgets/saved_item.dart | 15 +- mobile/lib/widgets/search_bar.dart | 7 +- mobile/lib/widgets/tts_toggle_fab.dart | 10 +- mobile/lib/widgets/turn_banner.dart | 9 +- mobile/pubspec.lock | 8 + mobile/pubspec.yaml | 45 ++-- mobile/test/api/paint_response_test.dart | 2 +- mobile/test/api/ratings_paint_api_test.dart | 2 +- mobile/test/api/routing_api_test.dart | 6 +- mobile/test/app_smoke_test.dart | 18 +- mobile/test/helpers/test_helpers.dart | 14 +- .../navigation/camera_controller_test.dart | 94 ++++--- .../navigation/location_converter_test.dart | 15 +- .../navigation/navigation_service_test.dart | 27 +- mobile/test/providers/auth_provider_test.dart | 27 +- .../test/providers/brush_controller_test.dart | 15 +- .../providers/navigation_provider_test.dart | 8 +- .../rating_overlay_controller_test.dart | 30 ++- .../test/providers/route_provider_test.dart | 16 +- .../screens/map_screen_navigation_test.dart | 8 +- mobile/test/screens/map_screen_test.dart | 24 +- .../test/screens/onboarding_screen_test.dart | 4 +- mobile/test/screens/search_screen_test.dart | 4 - .../screens/settings_danger_zone_test.dart | 5 +- mobile/test/screens/settings_login_test.dart | 8 +- mobile/test/services/brush_geometry_test.dart | 4 +- mobile/test/services/brush_overlay_test.dart | 4 +- .../services/rating_events_client_test.dart | 23 +- mobile/test/widgets/home_sheet_test.dart | 3 +- mobile/test/widgets/paint_sheet_test.dart | 7 +- mobile/test/widgets/rerouting_toast_test.dart | 6 +- mobile/test/widgets/route_card_test.dart | 24 +- mobile/test/widgets/route_summary_test.dart | 2 +- .../ferrostar_flutter/analysis_options.yaml | 15 +- .../example/analysis_options.yaml | 30 +-- .../plugin_integration_test.dart | 7 +- .../ferrostar_flutter/example/lib/main.dart | 11 +- .../ferrostar_flutter/example/pubspec.lock | 8 + .../ferrostar_flutter/example/pubspec.yaml | 21 +- .../example/test/widget_test.dart | 5 +- .../ferrostar_flutter/lib/src/controller.dart | 38 ++- .../ferrostar_flutter/lib/src/exceptions.dart | 31 ++- .../lib/src/ferrostar_flutter.dart | 21 +- .../lib/src/ferrostar_flutter_platform.dart | 38 ++- .../lib/src/method_channel_platform.dart | 55 ++-- .../lib/src/models/navigation_config.dart | 25 +- .../lib/src/models/navigation_state.dart | 46 +++- .../lib/src/models/route_deviation.dart | 13 +- .../lib/src/models/spoken_instruction.dart | 17 +- .../lib/src/models/trip_progress.dart | 12 +- .../lib/src/models/user_location.dart | 19 +- .../lib/src/models/visual_instruction.dart | 17 +- .../lib/src/models/waypoint_input.dart | 24 +- packages/ferrostar_flutter/pubspec.yaml | 7 +- .../test/controller_test.dart | 7 +- .../test/method_channel_platform_test.dart | 12 +- .../test/models/inputs_test.dart | 16 +- .../test/models/navigation_state_test.dart | 2 +- .../test/models/route_deviation_test.dart | 2 +- .../test/models/spoken_instruction_test.dart | 2 +- .../test/models/trip_progress_test.dart | 2 +- .../test/models/user_location_test.dart | 2 +- .../test/models/visual_instruction_test.dart | 2 +- 133 files changed, 1235 insertions(+), 1129 deletions(-) diff --git a/mobile/analysis_options.yaml b/mobile/analysis_options.yaml index 0d29021..e812ca5 100644 --- a/mobile/analysis_options.yaml +++ b/mobile/analysis_options.yaml @@ -1,28 +1,16 @@ -# This file configures the analyzer, which statically analyzes Dart code to -# check for errors, warnings, and lints. -# -# The issues identified by the analyzer are surfaced in the UI of Dart-enabled -# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be -# invoked from the command line by running `flutter analyze`. +include: package:very_good_analysis/analysis_options.yaml -# The following line activates a set of recommended lints for Flutter apps, -# packages, and plugins designed to encourage good coding practices. -include: package:flutter_lints/flutter.yaml +analyzer: + errors: + invalid_annotation_target: ignore + exclude: + - "**/*.g.dart" + - "**/*.freezed.dart" + - "lib/generated/**" linter: - # The lint rules applied to this project can be customized in the - # section below to disable rules from the `package:flutter_lints/flutter.yaml` - # included above or to enable additional rules. A list of all available lints - # and their documentation is published at https://dart.dev/lints. - # - # Instead of disabling a lint rule for the entire project in the - # section below, it can also be suppressed for a single line of code - # or a specific dart file by using the `// ignore: name_of_lint` and - # `// ignore_for_file: name_of_lint` syntax on the line or in the file - # producing the lint. rules: - # avoid_print: false # Uncomment to disable the `avoid_print` rule - # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule - -# Additional information about this file can be found at -# https://dart.dev/guides/language/analysis-options + # App code, not a published library. + public_member_api_docs: false + # `dart format` is authoritative for line length. + lines_longer_than_80_chars: false diff --git a/mobile/integration_test/brush_smoke_test.dart b/mobile/integration_test/brush_smoke_test.dart index 04226bb..16fe8c0 100644 --- a/mobile/integration_test/brush_smoke_test.dart +++ b/mobile/integration_test/brush_smoke_test.dart @@ -1,3 +1,6 @@ +import 'package:beebeebike/api/client.dart'; +import 'package:beebeebike/app.dart'; +import 'package:beebeebike/providers/brush_provider.dart'; import 'package:dio/dio.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; @@ -5,23 +8,18 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:http_mock_adapter/http_mock_adapter.dart'; import 'package:integration_test/integration_test.dart'; -import 'package:beebeebike/api/client.dart'; -import 'package:beebeebike/app.dart'; -import 'package:beebeebike/providers/brush_provider.dart'; - void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); testWidgets('paint FAB flips paint mode and shows PaintSheet', (tester) async { final dio = Dio(BaseOptions(baseUrl: 'http://test.local')); - final adapter = DioAdapter(dio: dio); - adapter - ..onGet('/api/auth/me', (r) => r.reply(401, {})) + DioAdapter(dio: dio) + ..onGet('/api/auth/me', (r) => r.reply(401, {})) ..onPost('/api/auth/anonymous', (r) => r.reply(200, {'id': 'anon', 'account_type': 'anonymous'})) ..onGet('/api/ratings', - (r) => r.reply(200, {'type': 'FeatureCollection', 'features': []})); + (r) => r.reply(200, {'type': 'FeatureCollection', 'features': []})); await tester.pumpWidget(ProviderScope( overrides: [dioProvider.overrideWithValue(dio)], diff --git a/mobile/integration_test/navigation_smoke_test.dart b/mobile/integration_test/navigation_smoke_test.dart index 82bc1b8..9fafb75 100644 --- a/mobile/integration_test/navigation_smoke_test.dart +++ b/mobile/integration_test/navigation_smoke_test.dart @@ -1,7 +1,9 @@ +import 'dart:async'; + +import 'package:beebeebike/main.dart' as app; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; -import 'package:beebeebike/main.dart' as app; void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); @@ -9,7 +11,7 @@ void main() { testWidgets( 'app launches and renders the map screen', (tester) async { - app.main(); + unawaited(app.main()); // MapLibre keeps frames coming, so pumpAndSettle can hang. Pump a // bounded number of frames instead and then assert the scaffold. for (var i = 0; i < 20; i++) { diff --git a/mobile/lib/api/auth_api.dart b/mobile/lib/api/auth_api.dart index 41c47de..b5b6796 100644 --- a/mobile/lib/api/auth_api.dart +++ b/mobile/lib/api/auth_api.dart @@ -1,20 +1,19 @@ +import 'package:beebeebike/models/user.dart'; import 'package:dio/dio.dart'; -import '../models/user.dart'; - class AuthApi { AuthApi(this._dio); final Dio _dio; Future anonymous() async => - User.fromJson((await _dio.post('/api/auth/anonymous')).data as Map); + User.fromJson((await _dio.post('/api/auth/anonymous')).data as Map); Future me() async => - User.fromJson((await _dio.get('/api/auth/me')).data as Map); + User.fromJson((await _dio.get('/api/auth/me')).data as Map); Future login(String email, String password) async => User.fromJson( - (await _dio.post('/api/auth/login', data: { + (await _dio.post('/api/auth/login', data: { 'email': email, 'password': password, })) @@ -23,7 +22,7 @@ class AuthApi { Future register(String email, String password, String? displayName) async => User.fromJson( - (await _dio.post('/api/auth/register', data: { + (await _dio.post('/api/auth/register', data: { 'email': email, 'password': password, 'display_name': displayName, @@ -32,10 +31,10 @@ class AuthApi { ); Future logout() async { - await _dio.post('/api/auth/logout'); + await _dio.post('/api/auth/logout'); } Future deleteAccount() async { - await _dio.delete('/api/auth/account'); + await _dio.delete('/api/auth/account'); } } diff --git a/mobile/lib/api/client.dart b/mobile/lib/api/client.dart index bfec1bc..91e8b5e 100644 --- a/mobile/lib/api/client.dart +++ b/mobile/lib/api/client.dart @@ -1,13 +1,12 @@ import 'dart:ui' as ui; +import 'package:beebeebike/app.dart'; +import 'package:beebeebike/providers/locale_provider.dart'; import 'package:cookie_jar/cookie_jar.dart'; import 'package:dio/dio.dart'; import 'package:dio_cookie_manager/dio_cookie_manager.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import '../app.dart'; -import '../providers/locale_provider.dart'; - /// Override this in main() with getApplicationSupportDirectory().path. final cookieStoragePathProvider = Provider( (_) => throw UnimplementedError('cookieStoragePathProvider not overridden'), diff --git a/mobile/lib/api/geocode_api.dart b/mobile/lib/api/geocode_api.dart index 50551a6..ac8164f 100644 --- a/mobile/lib/api/geocode_api.dart +++ b/mobile/lib/api/geocode_api.dart @@ -1,18 +1,19 @@ +import 'package:beebeebike/models/geocode_result.dart'; import 'package:dio/dio.dart'; -import '../models/geocode_result.dart'; - class GeocodeApi { GeocodeApi(this._dio); final Dio _dio; Future> search(String query) async { - final response = await _dio.get('/api/geocode', queryParameters: {'q': query}); - final features = (response.data['features'] as List).cast>(); + final response = await _dio.get('/api/geocode', queryParameters: {'q': query}); + final data = response.data as Map; + final features = (data['features'] as List).cast>(); return features.map((f) { final props = f['properties'] as Map; - final coords = (f['geometry']?['coordinates'] as List?) ?? [0.0, 0.0]; + final geometry = f['geometry'] as Map?; + final coords = (geometry?['coordinates'] as List?) ?? const [0, 0]; final rawName = (props['name'] as String?) ?? ''; final street = (props['street'] as String?) ?? ''; final housenumber = (props['housenumber'] as String?) ?? ''; diff --git a/mobile/lib/api/locations_api.dart b/mobile/lib/api/locations_api.dart index 91468ad..c8f15e2 100644 --- a/mobile/lib/api/locations_api.dart +++ b/mobile/lib/api/locations_api.dart @@ -1,7 +1,6 @@ +import 'package:beebeebike/models/location.dart'; import 'package:dio/dio.dart'; -import '../models/location.dart'; - class LocationsApi { LocationsApi(this._dio); @@ -9,7 +8,7 @@ class LocationsApi { Future getHome() async { try { - final response = await _dio.get('/api/locations/home'); + final response = await _dio.get('/api/locations/home'); final data = response.data; if (data == null) return null; return Location.fromJson(Map.from(data as Map)); @@ -20,7 +19,7 @@ class LocationsApi { } Future setHome(Location location) async { - final response = await _dio.put('/api/locations/home', data: { + final response = await _dio.put('/api/locations/home', data: { 'label': location.label, 'lng': location.lng, 'lat': location.lat, @@ -29,6 +28,6 @@ class LocationsApi { } Future deleteHome() async { - await _dio.delete('/api/locations/home'); + await _dio.delete('/api/locations/home'); } } diff --git a/mobile/lib/api/ratings_api.dart b/mobile/lib/api/ratings_api.dart index 0e98149..1e48f23 100644 --- a/mobile/lib/api/ratings_api.dart +++ b/mobile/lib/api/ratings_api.dart @@ -1,8 +1,7 @@ +import 'package:beebeebike/api/client.dart'; import 'package:dio/dio.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'client.dart'; - class RatingsApi { RatingsApi(this._dio); @@ -17,9 +16,9 @@ class RatingsApi { String bbox, { CancelToken? cancelToken, }) async { - final response = await _dio.get( + final response = await _dio.get( '/api/ratings', - queryParameters: {'bbox': bbox}, + queryParameters: {'bbox': bbox}, cancelToken: cancelToken, ); return Map.from(response.data as Map); diff --git a/mobile/lib/api/ratings_paint_api.dart b/mobile/lib/api/ratings_paint_api.dart index a19cd8f..91cd00b 100644 --- a/mobile/lib/api/ratings_paint_api.dart +++ b/mobile/lib/api/ratings_paint_api.dart @@ -1,9 +1,8 @@ +import 'package:beebeebike/api/client.dart'; +import 'package:beebeebike/models/paint_response.dart'; import 'package:dio/dio.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import '../models/paint_response.dart'; -import 'client.dart'; - class RatingsPaintApi { RatingsPaintApi(this._dio); @@ -14,9 +13,9 @@ class RatingsPaintApi { required int value, int? targetId, }) async { - final response = await _dio.put( + final response = await _dio.put( '/api/ratings/paint', - data: { + data: { 'geometry': geometry, 'value': value, 'target_id': targetId, @@ -28,14 +27,14 @@ class RatingsPaintApi { } Future undo() async { - final response = await _dio.post('/api/ratings/undo'); + final response = await _dio.post('/api/ratings/undo'); return PaintResponse.fromJson( Map.from(response.data as Map), ); } Future redo() async { - final response = await _dio.post('/api/ratings/redo'); + final response = await _dio.post('/api/ratings/redo'); return PaintResponse.fromJson( Map.from(response.data as Map), ); diff --git a/mobile/lib/api/routing_api.dart b/mobile/lib/api/routing_api.dart index 749aed1..fb48870 100644 --- a/mobile/lib/api/routing_api.dart +++ b/mobile/lib/api/routing_api.dart @@ -1,7 +1,6 @@ +import 'package:beebeebike/models/route_preview.dart'; import 'package:dio/dio.dart'; -import '../models/route_preview.dart'; - class RoutingApi { RoutingApi(this._dio); @@ -13,7 +12,7 @@ class RoutingApi { double? ratingWeight, double? distanceInfluence, }) async { - final response = await _dio.post('/api/route', data: { + final response = await _dio.post('/api/route', data: { 'origin': origin, 'destination': destination, if (ratingWeight != null) 'rating_weight': ratingWeight, @@ -28,7 +27,7 @@ class RoutingApi { double? ratingWeight, double? distanceInfluence, }) async { - final response = await _dio.post('/api/navigate', data: { + final response = await _dio.post('/api/navigate', data: { 'origin': origin, 'destination': destination, if (ratingWeight != null) 'rating_weight': ratingWeight, diff --git a/mobile/lib/app.dart b/mobile/lib/app.dart index 3012a6d..ba0c06e 100644 --- a/mobile/lib/app.dart +++ b/mobile/lib/app.dart @@ -1,15 +1,14 @@ +import 'package:beebeebike/config/app_config.dart'; +import 'package:beebeebike/l10n/generated/app_localizations.dart'; +import 'package:beebeebike/providers/auth_provider.dart'; +import 'package:beebeebike/providers/locale_provider.dart'; +import 'package:beebeebike/providers/onboarding_provider.dart'; +import 'package:beebeebike/screens/map_screen.dart'; +import 'package:beebeebike/screens/onboarding_screen.dart'; +import 'package:beebeebike/theme/app_theme.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'config/app_config.dart'; -import 'l10n/generated/app_localizations.dart'; -import 'providers/auth_provider.dart'; -import 'providers/locale_provider.dart'; -import 'providers/onboarding_provider.dart'; -import 'screens/map_screen.dart'; -import 'screens/onboarding_screen.dart'; -import 'theme/app_theme.dart'; - final appConfigProvider = Provider((ref) => AppConfig.fromEnvironment()); class BeeBeeBikeApp extends ConsumerWidget { diff --git a/mobile/lib/config/app_config.dart b/mobile/lib/config/app_config.dart index d77db17..9c850a0 100644 --- a/mobile/lib/config/app_config.dart +++ b/mobile/lib/config/app_config.dart @@ -10,6 +10,27 @@ class AppConfig { this.imprintUrl = 'https://beebeebike.com/impressum/', }); + factory AppConfig.fromEnvironment() { + return const AppConfig( + apiBaseUrl: String.fromEnvironment( + 'BEEBEEBIKE_API_BASE_URL', + defaultValue: 'http://127.0.0.1:3000', + ), + tileServerBaseUrl: String.fromEnvironment( + 'BEEBEEBIKE_TILE_SERVER_BASE_URL', + defaultValue: 'http://127.0.0.1:8080', + ), + tileStyleUrl: String.fromEnvironment( + 'BEEBEEBIKE_TILE_STYLE_URL', + defaultValue: 'http://127.0.0.1:8080/assets/styles/colorful/style.json', + ), + ratingsSseEnabled: bool.fromEnvironment( + 'BEEBEEBIKE_RATINGS_SSE_ENABLED', + defaultValue: true, + ), + ); + } + final String apiBaseUrl; final String tileServerBaseUrl; final String privacyPolicyUrl; @@ -33,41 +54,4 @@ class AppConfig { /// env when disabling globally — either flag off is enough to stop the /// push traffic. final bool ratingsSseEnabled; - - factory AppConfig.fromEnvironment() { - return const AppConfig( - apiBaseUrl: String.fromEnvironment( - 'BEEBEEBIKE_API_BASE_URL', - defaultValue: 'http://127.0.0.1:3000', - ), - tileServerBaseUrl: String.fromEnvironment( - 'BEEBEEBIKE_TILE_SERVER_BASE_URL', - defaultValue: 'http://127.0.0.1:8080', - ), - tileStyleUrl: String.fromEnvironment( - 'BEEBEEBIKE_TILE_STYLE_URL', - defaultValue: 'http://127.0.0.1:8080/assets/styles/colorful/style.json', - ), - ratingsSseEnabled: bool.fromEnvironment( - 'BEEBEEBIKE_RATINGS_SSE_ENABLED', - defaultValue: true, - ), - privacyPolicyUrl: String.fromEnvironment( - 'BEEBEEBIKE_PRIVACY_POLICY_URL', - defaultValue: 'https://beebeebike.com/datenschutz/', - ), - imprintUrl: String.fromEnvironment( - 'BEEBEEBIKE_IMPRINT_URL', - defaultValue: 'https://beebeebike.com/impressum/', - ), - glitchtipDsn: String.fromEnvironment( - 'BEEBEEBIKE_GLITCHTIP_DSN', - defaultValue: '', - ), - environment: String.fromEnvironment( - 'BEEBEEBIKE_ENVIRONMENT', - defaultValue: 'development', - ), - ); - } } diff --git a/mobile/lib/main.dart b/mobile/lib/main.dart index c8329ba..deebf7f 100644 --- a/mobile/lib/main.dart +++ b/mobile/lib/main.dart @@ -1,16 +1,15 @@ import 'dart:async'; +import 'package:beebeebike/api/client.dart'; +import 'package:beebeebike/app.dart'; +import 'package:beebeebike/config/app_config.dart'; +import 'package:beebeebike/providers/search_history_provider.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:path_provider/path_provider.dart'; import 'package:sentry_flutter/sentry_flutter.dart'; import 'package:shared_preferences/shared_preferences.dart'; -import 'app.dart'; -import 'api/client.dart'; -import 'config/app_config.dart'; -import 'providers/search_history_provider.dart'; - Future main() async { final config = AppConfig.fromEnvironment(); @@ -37,10 +36,11 @@ Future main() async { await SentryFlutter.init( (options) { - options.dsn = config.glitchtipDsn; - options.environment = config.environment; - options.tracesSampleRate = 0.0; - options.attachStacktrace = true; + options + ..dsn = config.glitchtipDsn + ..environment = config.environment + ..tracesSampleRate = 0.0 + ..attachStacktrace = true; // GlitchTip does not support session replay / profiling; keep off. }, appRunner: bootstrap, diff --git a/mobile/lib/models/paint_response.dart b/mobile/lib/models/paint_response.dart index 60caccf..56b0298 100644 --- a/mobile/lib/models/paint_response.dart +++ b/mobile/lib/models/paint_response.dart @@ -1,4 +1,4 @@ -// ignore_for_file: invalid_annotation_target +// ignore_for_file: invalid_annotation_target, freezed/json_serializable produce annotations on getters that this rule otherwise flags. import 'package:freezed_annotation/freezed_annotation.dart'; part 'paint_response.freezed.dart'; diff --git a/mobile/lib/models/route_state.dart b/mobile/lib/models/route_state.dart index 41df446..02a24f7 100644 --- a/mobile/lib/models/route_state.dart +++ b/mobile/lib/models/route_state.dart @@ -1,8 +1,7 @@ +import 'package:beebeebike/models/location.dart'; +import 'package:beebeebike/models/route_preview.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; -import 'location.dart'; -import 'route_preview.dart'; - part 'route_state.freezed.dart'; @freezed diff --git a/mobile/lib/models/user.dart b/mobile/lib/models/user.dart index 3f2dd30..bd36870 100644 --- a/mobile/lib/models/user.dart +++ b/mobile/lib/models/user.dart @@ -1,4 +1,4 @@ -// ignore_for_file: invalid_annotation_target +// ignore_for_file: invalid_annotation_target, freezed/json_serializable produce annotations on getters that this rule otherwise flags. import 'package:freezed_annotation/freezed_annotation.dart'; part 'user.freezed.dart'; @@ -8,9 +8,8 @@ part 'user.g.dart'; class User with _$User { const factory User({ required String id, - String? email, + @JsonKey(name: 'account_type') required String accountType, String? email, @Default('') @JsonKey(name: 'display_name') String displayName, - @JsonKey(name: 'account_type') required String accountType, }) = _User; factory User.fromJson(Map json) => _$UserFromJson(json); diff --git a/mobile/lib/navigation/camera_controller.dart b/mobile/lib/navigation/camera_controller.dart index cc8fb96..2d755bc 100644 --- a/mobile/lib/navigation/camera_controller.dart +++ b/mobile/lib/navigation/camera_controller.dart @@ -4,7 +4,7 @@ enum CameraMode { awaitingFirstFix, following, free, arrived } class NavigationCameraController extends ChangeNotifier { CameraMode _mode = CameraMode.awaitingFirstFix; - double _followZoom = 17.0; + double _followZoom = 17; CameraMode get mode => _mode; double get followZoom => _followZoom; diff --git a/mobile/lib/navigation/nav_constants.dart b/mobile/lib/navigation/nav_constants.dart index 37eabb9..5994fd9 100644 --- a/mobile/lib/navigation/nav_constants.dart +++ b/mobile/lib/navigation/nav_constants.dart @@ -5,8 +5,8 @@ library; /// Approximate rendered height of the ETA bottom sheet. Used to offset the /// RecenterFab so it never overlaps the sheet. -const double kEtaSheetHeight = 120.0; +const double kEtaSheetHeight = 120; /// Zoom used when flying the camera to the destination on arrival. Matches /// the nav-camera design spec (Q4). -const double kArrivalZoom = 17.0; +const double kArrivalZoom = 17; diff --git a/mobile/lib/navigation/navigation_service.dart b/mobile/lib/navigation/navigation_service.dart index 65d8634..46c86f3 100644 --- a/mobile/lib/navigation/navigation_service.dart +++ b/mobile/lib/navigation/navigation_service.dart @@ -1,11 +1,10 @@ import 'dart:async'; +import 'package:beebeebike/services/error_reporter.dart'; +import 'package:beebeebike/services/haptics.dart'; import 'package:ferrostar_flutter/ferrostar_flutter.dart'; import 'package:flutter/foundation.dart'; -import '../services/error_reporter.dart'; -import '../services/haptics.dart'; - typedef CreateController = Future Function( Map osrmJson, List waypoints, @@ -16,7 +15,7 @@ typedef LoadNavigationRoute = Future> Function({ }); typedef SpeakInstruction = Future Function(String text); typedef LocationStreamFactory = Stream Function(); -typedef SetWakelock = Future Function(bool enabled); +typedef SetWakelock = Future Function({required bool enabled}); class NavigationService { NavigationService({ @@ -27,7 +26,7 @@ class NavigationService { SetWakelock? setWakelock, }) : setWakelock = setWakelock ?? _noopWakelock; - static Future _noopWakelock(bool _) async {} + static Future _noopWakelock({required bool enabled}) async {} final CreateController createController; final LoadNavigationRoute loadNavigationRoute; @@ -99,7 +98,7 @@ class NavigationService { // utterance once. if (instruction.uuid == _lastSpokenUuid) return; _lastSpokenUuid = instruction.uuid; - speakInstruction(instruction.text); + unawaited(speakInstruction(instruction.text)); }, ); @@ -107,7 +106,7 @@ class NavigationService { if (_rerouteInProgress) return; _rerouteInProgress = true; _rerouteController.add(true); - AppHaptics.offRoute(); + unawaited(AppHaptics.offRoute()); debugPrint( 'nav: deviation ${deviation.deviationM.toStringAsFixed(0)}m, rerouting'); try { @@ -121,7 +120,7 @@ class NavigationService { if (_controller == null) return; await controller.replaceRoute(rerouteJson); _rerouteSucceededController.add(null); - } catch (e, st) { + } on Object catch (e, st) { reportError(e, st, context: 'nav.reroute'); } finally { _rerouteInProgress = false; @@ -130,13 +129,16 @@ class NavigationService { }); _locationSub = locationStreamFactory().listen( - (location) => _controller?.updateLocation(location), + (location) { + final controller = _controller; + if (controller != null) unawaited(controller.updateLocation(location)); + }, ); try { - await setWakelock(true); + await setWakelock(enabled: true); _wakelockActive = true; - } catch (e, st) { + } on Object catch (e, st) { reportError(e, st, context: 'nav.wakelock.enable'); } } @@ -144,8 +146,8 @@ class NavigationService { Future dispose() async { if (_wakelockActive) { try { - await setWakelock(false); - } catch (e, st) { + await setWakelock(enabled: false); + } on Object catch (e, st) { reportError(e, st, context: 'nav.wakelock.disable'); } _wakelockActive = false; diff --git a/mobile/lib/providers/auth_provider.dart b/mobile/lib/providers/auth_provider.dart index 57d611d..71a8d18 100644 --- a/mobile/lib/providers/auth_provider.dart +++ b/mobile/lib/providers/auth_provider.dart @@ -1,10 +1,9 @@ +import 'package:beebeebike/api/auth_api.dart'; +import 'package:beebeebike/api/client.dart'; +import 'package:beebeebike/models/user.dart'; import 'package:dio/dio.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import '../api/auth_api.dart'; -import '../api/client.dart'; -import '../models/user.dart'; - final authApiProvider = Provider((ref) => AuthApi(ref.watch(dioProvider))); final authControllerProvider = diff --git a/mobile/lib/providers/brush_provider.dart b/mobile/lib/providers/brush_provider.dart index 1b677ce..43e6b2e 100644 --- a/mobile/lib/providers/brush_provider.dart +++ b/mobile/lib/providers/brush_provider.dart @@ -1,16 +1,15 @@ import 'dart:async'; +import 'package:beebeebike/api/ratings_paint_api.dart'; +import 'package:beebeebike/providers/rating_overlay_provider.dart'; +import 'package:beebeebike/providers/route_provider.dart'; +import 'package:beebeebike/services/brush_geometry.dart'; +import 'package:beebeebike/services/brush_overlay.dart'; +import 'package:beebeebike/services/error_reporter.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:maplibre_gl/maplibre_gl.dart'; -import '../api/ratings_paint_api.dart'; -import '../providers/rating_overlay_provider.dart'; -import '../providers/route_provider.dart'; -import '../services/brush_geometry.dart'; -import '../services/brush_overlay.dart'; -import '../services/error_reporter.dart'; - class TapFeature { const TapFeature({required this.areaId, required this.geometry}); final int areaId; @@ -72,7 +71,8 @@ class BrushController extends Notifier { @override BrushState build() => const BrushState(); - void attach({required BrushOverlaySurface surface}) { + // ignore: use_setters_to_change_properties, paired with avoid_setters_without_getters; the rule conflicts here. + void attach(BrushOverlaySurface surface) { _overlay = surface; } @@ -159,7 +159,7 @@ class BrushController extends Notifier { .refreshAfterPaint(); state = state.copyWith(canUndo: r.canUndo, canRedo: r.canRedo); _maybeRecomputeRoute(); - } catch (e, st) { + } on Object catch (e, st) { _log('brush: undo failed: $e'); reportError(e, st, context: 'brush.undo'); } finally { @@ -178,7 +178,7 @@ class BrushController extends Notifier { .refreshAfterPaint(); state = state.copyWith(canUndo: r.canUndo, canRedo: r.canRedo); _maybeRecomputeRoute(); - } catch (e, st) { + } on Object catch (e, st) { _log('brush: redo failed: $e'); reportError(e, st, context: 'brush.redo'); } finally { @@ -222,7 +222,7 @@ class BrushController extends Notifier { } state = state.copyWith(canUndo: r.canUndo, canRedo: r.canRedo); _maybeRecomputeRoute(); - } catch (e, st) { + } on Object catch (e, st) { _log('brush: paint failed: $e'); reportError(e, st, context: 'brush.paint'); } finally { diff --git a/mobile/lib/providers/home_eta_provider.dart b/mobile/lib/providers/home_eta_provider.dart index 5a1ad7c..f383d24 100644 --- a/mobile/lib/providers/home_eta_provider.dart +++ b/mobile/lib/providers/home_eta_provider.dart @@ -1,10 +1,9 @@ +import 'package:beebeebike/models/location.dart'; +import 'package:beebeebike/providers/location_provider.dart'; +import 'package:beebeebike/providers/route_provider.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:geolocator/geolocator.dart'; -import '../models/location.dart'; -import 'location_provider.dart'; -import 'route_provider.dart'; - /// Coarse position stream — emits only on significant movement (≥200 m). /// Drives periodic ETA refresh without hitting the router on every GPS tick. final significantPositionProvider = StreamProvider((ref) { @@ -24,12 +23,12 @@ final homeEtaMinutesProvider = FutureProvider((ref) async { if (home == null) return null; final streamed = ref.watch(significantPositionProvider).valueOrNull; - Position? pos = streamed; + var pos = streamed; if (pos == null) { try { pos = await Geolocator.getLastKnownPosition() ?? await Geolocator.getCurrentPosition(); - } catch (_) { + } on Object catch (_) { return null; } } diff --git a/mobile/lib/providers/locale_provider.dart b/mobile/lib/providers/locale_provider.dart index 853179d..89f7c06 100644 --- a/mobile/lib/providers/locale_provider.dart +++ b/mobile/lib/providers/locale_provider.dart @@ -1,8 +1,7 @@ +import 'package:beebeebike/providers/search_history_provider.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'search_history_provider.dart'; - /// User language preference. `system` defers to the device locale; `en` and /// `de` pin the app. enum LocalePref { system, en, de } diff --git a/mobile/lib/providers/location_provider.dart b/mobile/lib/providers/location_provider.dart index 07455d1..4f7a079 100644 --- a/mobile/lib/providers/location_provider.dart +++ b/mobile/lib/providers/location_provider.dart @@ -1,10 +1,9 @@ +import 'package:beebeebike/api/client.dart'; +import 'package:beebeebike/api/locations_api.dart'; +import 'package:beebeebike/models/location.dart'; +import 'package:beebeebike/providers/auth_provider.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import '../api/client.dart'; -import '../api/locations_api.dart'; -import '../models/location.dart'; -import 'auth_provider.dart'; - final locationsApiProvider = Provider( (ref) => LocationsApi(ref.watch(dioProvider)), ); diff --git a/mobile/lib/providers/navigation_camera_provider.dart b/mobile/lib/providers/navigation_camera_provider.dart index 51f73bd..98f65ea 100644 --- a/mobile/lib/providers/navigation_camera_provider.dart +++ b/mobile/lib/providers/navigation_camera_provider.dart @@ -1,8 +1,7 @@ +import 'package:beebeebike/navigation/camera_controller.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import '../navigation/camera_controller.dart'; - -final navigationCameraControllerProvider = +final AutoDisposeChangeNotifierProvider navigationCameraControllerProvider = ChangeNotifierProvider.autoDispose((ref) { final controller = NavigationCameraController(); return controller; diff --git a/mobile/lib/providers/navigation_provider.dart b/mobile/lib/providers/navigation_provider.dart index 261c6e5..758db64 100644 --- a/mobile/lib/providers/navigation_provider.dart +++ b/mobile/lib/providers/navigation_provider.dart @@ -1,6 +1,13 @@ +import 'dart:async'; import 'dart:io' show Platform; import 'dart:ui' as ui; +import 'package:beebeebike/api/client.dart'; +import 'package:beebeebike/api/routing_api.dart'; +import 'package:beebeebike/navigation/location_converter.dart'; +import 'package:beebeebike/navigation/navigation_service.dart'; +import 'package:beebeebike/providers/locale_provider.dart'; +import 'package:beebeebike/services/error_reporter.dart'; import 'package:ferrostar_flutter/ferrostar_flutter.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; @@ -8,13 +15,6 @@ import 'package:flutter_tts/flutter_tts.dart'; import 'package:geolocator/geolocator.dart'; import 'package:wakelock_plus/wakelock_plus.dart'; -import '../api/client.dart'; -import '../api/routing_api.dart'; -import '../navigation/location_converter.dart'; -import '../navigation/navigation_service.dart'; -import '../services/error_reporter.dart'; -import 'locale_provider.dart'; - Stream _buildLocationStream() async* { var permission = await Geolocator.checkPermission(); if (permission == LocationPermission.denied) { @@ -41,15 +41,12 @@ Stream _buildLocationStream() async* { ? AppleSettings( accuracy: LocationAccuracy.bestForNavigation, activityType: ActivityType.otherNavigation, - distanceFilter: 0, - pauseLocationUpdatesAutomatically: false, allowBackgroundLocationUpdates: permission == LocationPermission.always, showBackgroundLocationIndicator: true, ) : const LocationSettings( accuracy: LocationAccuracy.bestForNavigation, - distanceFilter: 0, ); yield* Geolocator.getPositionStream(locationSettings: settings) .map(positionToUserLocation); @@ -67,26 +64,28 @@ final flutterTtsProvider = Provider((ref) { final deviceLocale = ui.PlatformDispatcher.instance.locale; final tag = effectiveLanguageTag(pref, deviceLocale); final ttsTag = tag == 'de' ? 'de-DE' : 'en-US'; - tts.setLanguage(ttsTag); + unawaited(tts.setLanguage(ttsTag)); // Configure the iOS audio session so voice cues keep playing when the // screen is locked and duck (rather than stop) any music the rider has // running. Requires `UIBackgroundModes: audio` in Info.plist. if (!kIsWeb && Platform.isIOS) { - tts.setSharedInstance(true); - tts.setIosAudioCategory( - IosTextToSpeechAudioCategory.playback, - [ - IosTextToSpeechAudioCategoryOptions.duckOthers, - IosTextToSpeechAudioCategoryOptions.allowBluetooth, - IosTextToSpeechAudioCategoryOptions.allowBluetoothA2DP, - ], - IosTextToSpeechAudioMode.voicePrompt, + unawaited(tts.setSharedInstance(true)); + unawaited( + tts.setIosAudioCategory( + IosTextToSpeechAudioCategory.playback, + [ + IosTextToSpeechAudioCategoryOptions.duckOthers, + IosTextToSpeechAudioCategoryOptions.allowBluetooth, + IosTextToSpeechAudioCategoryOptions.allowBluetoothA2DP, + ], + IosTextToSpeechAudioMode.voicePrompt, + ), ); } // Fire-and-forget: upgrade to the best locally installed voice. iOS ships // a "default" voice per language; users who downloaded Enhanced/Premium // voices get a much better sounding cue without any extra UI. - _applyBestVoice(tts, ttsTag); + unawaited(_applyBestVoice(tts, ttsTag)); return tts; }); @@ -122,7 +121,7 @@ Future _applyBestVoice(FlutterTts tts, String localeTag) async { 'locale': best['locale']!, if (best['identifier']!.isNotEmpty) 'identifier': best['identifier']!, }); - } catch (e) { + } on Object catch (e) { debugPrint('nav: tts voice pick failed: $e'); } } @@ -140,12 +139,12 @@ final navigationServiceProvider = Provider((ref) { loadNavigationRoute: ({required origin, required destination}) => routingApi.computeNavigationRoute(origin, destination), locationStreamFactory: _buildLocationStream, - setWakelock: (enabled) => WakelockPlus.toggle(enable: enabled), + setWakelock: ({required enabled}) => WakelockPlus.toggle(enable: enabled), speakInstruction: (text) async { if (!ref.read(ttsEnabledProvider)) return; try { await tts.speak(text); - } catch (e) { + } on Object catch (e) { // TTS speak fails routinely on audio-session interruptions, silent // mode switches, and mid-utterance cancellations. Drop a breadcrumb // for context around nearby issues but don't surface as its own @@ -156,14 +155,14 @@ final navigationServiceProvider = Provider((ref) { ); }); -final navigationStateProvider = StreamProvider.autoDispose((ref) { +final AutoDisposeStreamProvider navigationStateProvider = StreamProvider.autoDispose((ref) { return ref.watch(navigationServiceProvider).stateStream; }); -final rerouteInProgressProvider = StreamProvider.autoDispose((ref) { +final AutoDisposeStreamProvider rerouteInProgressProvider = StreamProvider.autoDispose((ref) { return ref.watch(navigationServiceProvider).rerouteInProgressStream; }); -final rerouteSucceededProvider = StreamProvider.autoDispose((ref) { +final AutoDisposeStreamProvider rerouteSucceededProvider = StreamProvider.autoDispose((ref) { return ref.watch(navigationServiceProvider).rerouteSucceededStream; }); diff --git a/mobile/lib/providers/onboarding_provider.dart b/mobile/lib/providers/onboarding_provider.dart index 9541e10..7893378 100644 --- a/mobile/lib/providers/onboarding_provider.dart +++ b/mobile/lib/providers/onboarding_provider.dart @@ -1,7 +1,6 @@ +import 'package:beebeebike/providers/search_history_provider.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'search_history_provider.dart'; - /// Flip to `true` locally to replay the onboarding flow on every launch /// regardless of the persisted flag. Must stay `false` on committed code. const kAlwaysShowOnboarding = false; diff --git a/mobile/lib/providers/rating_overlay_provider.dart b/mobile/lib/providers/rating_overlay_provider.dart index 044db23..b572289 100644 --- a/mobile/lib/providers/rating_overlay_provider.dart +++ b/mobile/lib/providers/rating_overlay_provider.dart @@ -1,19 +1,19 @@ import 'dart:async'; +import 'package:beebeebike/api/ratings_api.dart'; +import 'package:beebeebike/app.dart'; +import 'package:beebeebike/config/app_config.dart' show AppConfig; +import 'package:beebeebike/config/berlin_bounds.dart'; +import 'package:beebeebike/models/user.dart'; +import 'package:beebeebike/providers/auth_provider.dart'; +import 'package:beebeebike/services/error_reporter.dart'; +import 'package:beebeebike/services/rating_events_client.dart'; +import 'package:beebeebike/services/rating_overlay.dart'; import 'package:dio/dio.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:maplibre_gl/maplibre_gl.dart'; -import '../api/ratings_api.dart'; -import '../app.dart'; -import '../config/berlin_bounds.dart'; -import '../models/user.dart'; -import '../services/error_reporter.dart'; -import '../services/rating_events_client.dart'; -import '../services/rating_overlay.dart'; -import 'auth_provider.dart'; - /// Async function that builds and attaches a [RatingOverlaySurface]. Called /// once from [RatingOverlayController.attach]; allowed to throw if the /// underlying style isn't ready — the controller logs and bails. @@ -81,13 +81,13 @@ class RatingOverlayController extends Notifier { _log('rating-overlay: reattaching (prior overlay discarded)'); try { await prior.detach(); - } catch (_) {} + } on Object catch (_) {} } _log('rating-overlay: attach start'); try { _overlay = await attachOverlay(); _log('rating-overlay: attach ok'); - } catch (e, st) { + } on Object catch (e, st) { _log('rating-overlay: attach FAILED: $e\n$st'); reportError(e, st, context: 'rating-overlay.attach'); return; @@ -97,7 +97,7 @@ class RatingOverlayController extends Notifier { // client keeps running across view recreation, so we only need the // one-shot sync here. if (ref.read(authControllerProvider).valueOrNull != null) { - _fullSync(); + unawaited(_fullSync()); } return; } @@ -112,13 +112,14 @@ class RatingOverlayController extends Notifier { final nextId = next.valueOrNull?.id; _log('rating-overlay: auth change $prevId -> $nextId'); if (prevId != nextId) { - _overlay?.clear(); + final overlay = _overlay; + if (overlay != null) unawaited(overlay.clear()); // Restart the SSE stream against the new session cookie so // invalidations are filtered for the right user. The client is // keyed to a Dio instance, not a user id, so stopping and starting // is cheap — it just drops the held socket. unawaited(_restartEventsClient()); - if (nextId != null) _fullSync(); + if (nextId != null) unawaited(_fullSync()); } }); // Kick off an initial fetch only if auth is already resolved. If it's @@ -127,7 +128,7 @@ class RatingOverlayController extends Notifier { // `.value` rethrows on `AsyncError`, which happens in CI integration // tests where anonymous-auth gets "connection refused". if (ref.read(authControllerProvider).valueOrNull != null) { - _fullSync(); + unawaited(_fullSync()); _startEventsClient(); } } @@ -172,7 +173,7 @@ class RatingOverlayController extends Notifier { /// [RatingEventsClient]). Every invalidate triggers one full sync. void _onInvalidate() { _log('rating-overlay: invalidate from server'); - _fullSync(); + unawaited(_fullSync()); } void _markLiveSyncDegraded() { @@ -248,7 +249,7 @@ class RatingOverlayController extends Notifier { if (CancelToken.isCancel(e)) return; _log('rating-overlay: sync failed status=${e.response?.statusCode} ' 'type=${e.type} msg=${e.message}'); - } catch (e, st) { + } on Object catch (e, st) { _log('rating-overlay: sync failed: $e'); reportError(e, st, context: 'rating-overlay.sync'); } finally { diff --git a/mobile/lib/providers/route_provider.dart b/mobile/lib/providers/route_provider.dart index ede16a3..8ded376 100644 --- a/mobile/lib/providers/route_provider.dart +++ b/mobile/lib/providers/route_provider.dart @@ -1,13 +1,14 @@ -import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'dart:async'; -import '../api/client.dart'; -import '../api/routing_api.dart'; -import '../models/location.dart'; -import '../models/route_preview.dart'; -import '../models/route_state.dart'; -import '../models/user.dart'; -import '../providers/auth_provider.dart'; -import '../services/haptics.dart'; +import 'package:beebeebike/api/client.dart'; +import 'package:beebeebike/api/routing_api.dart'; +import 'package:beebeebike/models/location.dart'; +import 'package:beebeebike/models/route_preview.dart'; +import 'package:beebeebike/models/route_state.dart'; +import 'package:beebeebike/models/user.dart'; +import 'package:beebeebike/providers/auth_provider.dart'; +import 'package:beebeebike/services/haptics.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; typedef RoutePreviewLoader = Future Function({ required Location origin, @@ -38,7 +39,7 @@ class RouteController extends Notifier { if (prevUser?.accountType == 'anonymous' && nextUser != null && nextUser.accountType != 'anonymous') { - _maybeLoadPreview(); + unawaited(_maybeLoadPreview()); } }); return const RouteState(); @@ -66,13 +67,13 @@ class RouteController extends Notifier { error: 'Origin and destination are the same', preview: null, ); - AppHaptics.routeError(); + unawaited(AppHaptics.routeError()); return; } final generation = ++_loadGeneration; state = state.copyWith(isLoading: true, error: null, preview: null); - AppHaptics.routeCalcStart(); + unawaited(AppHaptics.routeCalcStart()); try { final preview = await ref.read(routePreviewLoaderProvider)( origin: origin, @@ -80,11 +81,11 @@ class RouteController extends Notifier { ); if (generation != _loadGeneration) return; state = state.copyWith(preview: preview, isLoading: false); - AppHaptics.routeSuccess(); - } catch (error) { + unawaited(AppHaptics.routeSuccess()); + } on Object catch (error) { if (generation != _loadGeneration) return; state = state.copyWith(isLoading: false, error: error.toString()); - AppHaptics.routeError(); + unawaited(AppHaptics.routeError()); } } @@ -111,7 +112,7 @@ class RouteController extends Notifier { ); if (generation != _loadGeneration) return; state = state.copyWith(preview: preview, isLoading: false); - } catch (error) { + } on Object catch (error) { if (generation != _loadGeneration) return; state = state.copyWith(isLoading: false, error: error.toString()); } diff --git a/mobile/lib/providers/search_history_provider.dart b/mobile/lib/providers/search_history_provider.dart index 4460261..3d2b1a0 100644 --- a/mobile/lib/providers/search_history_provider.dart +++ b/mobile/lib/providers/search_history_provider.dart @@ -1,10 +1,9 @@ import 'dart:convert'; +import 'package:beebeebike/models/location.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:shared_preferences/shared_preferences.dart'; -import '../models/location.dart'; - const _recentSearchesKey = 'beebeebike.recentSearches'; final sharedPreferencesProvider = Provider((_) { diff --git a/mobile/lib/screens/legal_document_screen.dart b/mobile/lib/screens/legal_document_screen.dart index c51da72..4df2be2 100644 --- a/mobile/lib/screens/legal_document_screen.dart +++ b/mobile/lib/screens/legal_document_screen.dart @@ -1,15 +1,14 @@ +import 'dart:async'; + +import 'package:beebeebike/l10n/generated/app_localizations.dart'; +import 'package:beebeebike/theme/tokens.dart'; import 'package:flutter/material.dart'; import 'package:url_launcher/url_launcher.dart'; import 'package:webview_flutter/webview_flutter.dart'; -import '../l10n/generated/app_localizations.dart'; -import '../theme/tokens.dart'; - class LegalDocumentScreen extends StatefulWidget { const LegalDocumentScreen({ - super.key, - required this.title, - required this.url, + required this.title, required this.url, super.key, }); final String title; @@ -27,22 +26,26 @@ class _LegalDocumentScreenState extends State { @override void initState() { super.initState(); - _controller = WebViewController() - ..setJavaScriptMode(JavaScriptMode.unrestricted) - ..setBackgroundColor(BbbColors.bg) - ..setNavigationDelegate( - NavigationDelegate( - onPageFinished: (_) => setState(() => _loading = false), - onWebResourceError: (error) { - if (!(error.isForMainFrame ?? true)) return; - setState(() { - _loading = false; - _error = error.description; - }); - }, - ), - ) - ..loadRequest(_embedUri(widget.url)); + _controller = WebViewController(); + unawaited(_initController()); + } + + Future _initController() async { + await _controller.setJavaScriptMode(JavaScriptMode.unrestricted); + await _controller.setBackgroundColor(BbbColors.bg); + await _controller.setNavigationDelegate( + NavigationDelegate( + onPageFinished: (_) => setState(() => _loading = false), + onWebResourceError: (error) { + if (!(error.isForMainFrame ?? true)) return; + setState(() { + _loading = false; + _error = error.description; + }); + }, + ), + ); + await _controller.loadRequest(_embedUri(widget.url)); } Uri _embedUri(String url) { diff --git a/mobile/lib/screens/login_screen.dart b/mobile/lib/screens/login_screen.dart index ee1ab62..50d29c0 100644 --- a/mobile/lib/screens/login_screen.dart +++ b/mobile/lib/screens/login_screen.dart @@ -1,8 +1,7 @@ +import 'package:beebeebike/l10n/generated/app_localizations.dart'; +import 'package:beebeebike/widgets/login_form.dart'; import 'package:flutter/material.dart'; -import '../l10n/generated/app_localizations.dart'; -import '../widgets/login_form.dart'; - class LoginScreen extends StatelessWidget { const LoginScreen({super.key, this.onSuccess}); diff --git a/mobile/lib/screens/map_screen.dart b/mobile/lib/screens/map_screen.dart index ca7f995..2361f71 100644 --- a/mobile/lib/screens/map_screen.dart +++ b/mobile/lib/screens/map_screen.dart @@ -1,48 +1,47 @@ import 'dart:async'; import 'dart:math' as math; +import 'package:beebeebike/l10n/generated/app_localizations.dart'; +import 'package:beebeebike/models/location.dart'; +import 'package:beebeebike/models/route_state.dart'; +import 'package:beebeebike/navigation/camera_controller.dart'; +import 'package:beebeebike/navigation/location_converter.dart'; +import 'package:beebeebike/navigation/nav_constants.dart'; +import 'package:beebeebike/providers/brush_provider.dart'; +import 'package:beebeebike/providers/location_provider.dart'; +import 'package:beebeebike/providers/map_bearing_provider.dart'; +import 'package:beebeebike/providers/navigation_camera_provider.dart'; +import 'package:beebeebike/providers/navigation_provider.dart'; +import 'package:beebeebike/providers/navigation_session_provider.dart'; +import 'package:beebeebike/providers/rating_overlay_provider.dart'; +import 'package:beebeebike/providers/route_provider.dart'; +import 'package:beebeebike/providers/user_location_provider.dart'; +import 'package:beebeebike/services/brush_overlay.dart'; +import 'package:beebeebike/services/error_reporter.dart'; +import 'package:beebeebike/services/haptics.dart'; +import 'package:beebeebike/services/home_marker_service.dart'; +import 'package:beebeebike/services/map_style_loader.dart'; +import 'package:beebeebike/services/rating_overlay.dart'; +import 'package:beebeebike/services/route_drawing.dart'; +import 'package:beebeebike/theme/tokens.dart'; +import 'package:beebeebike/widgets/brush_fab.dart'; +import 'package:beebeebike/widgets/map/home_sheet_container.dart'; +import 'package:beebeebike/widgets/map/nav_top_bar.dart'; +import 'package:beebeebike/widgets/map/navigation_sheet.dart'; +import 'package:beebeebike/widgets/map/route_sheet.dart'; +import 'package:beebeebike/widgets/paint_sheet.dart'; +import 'package:beebeebike/widgets/route_card.dart'; import 'package:ferrostar_flutter/ferrostar_flutter.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:geolocator/geolocator.dart'; -import 'package:maplibre_gl/maplibre_gl.dart' hide UserLocation; import 'package:maplibre_gl/maplibre_gl.dart' as ml show UserLocation; - -import '../l10n/generated/app_localizations.dart'; -import '../models/location.dart'; -import '../models/route_state.dart'; -import '../navigation/camera_controller.dart'; -import '../navigation/location_converter.dart'; -import '../navigation/nav_constants.dart'; -import '../providers/brush_provider.dart'; -import '../providers/navigation_camera_provider.dart'; -import '../providers/navigation_provider.dart'; -import '../providers/navigation_session_provider.dart'; -import '../providers/location_provider.dart'; -import '../providers/map_bearing_provider.dart'; -import '../providers/rating_overlay_provider.dart'; -import '../providers/route_provider.dart'; -import '../providers/user_location_provider.dart'; -import '../services/brush_overlay.dart'; -import '../services/error_reporter.dart'; -import '../services/haptics.dart'; -import '../services/home_marker_service.dart'; -import '../services/map_style_loader.dart'; -import '../services/rating_overlay.dart'; -import '../services/route_drawing.dart'; -import '../theme/tokens.dart'; -import '../widgets/brush_fab.dart'; -import '../widgets/map/home_sheet_container.dart'; -import '../widgets/map/nav_top_bar.dart'; -import '../widgets/map/navigation_sheet.dart'; -import '../widgets/map/route_sheet.dart'; -import '../widgets/paint_sheet.dart'; -import '../widgets/route_card.dart'; +import 'package:maplibre_gl/maplibre_gl.dart' hide UserLocation; final _berlinBounds = LatLngBounds( - southwest: const LatLng(52.3, 13.0), + southwest: const LatLng(52.3, 13), northeast: const LatLng(52.7, 13.8), ); @@ -74,9 +73,9 @@ class _MapScreenState extends ConsumerState { if (ref.read(routeControllerProvider).origin == null) { final origin = await _resolveCurrentOriginLocation(l10n); if (!mounted) return; - notifier.setOrigin(origin); + unawaited(notifier.setOrigin(origin)); } - notifier.setDestination( + unawaited(notifier.setDestination( Location( id: 'geo:${coords.latitude},${coords.longitude}', name: @@ -85,7 +84,7 @@ class _MapScreenState extends ConsumerState { lng: coords.longitude, lat: coords.latitude, ), - ); + )); } Future _onRouteStateChanged( @@ -116,9 +115,9 @@ class _MapScreenState extends ConsumerState { math.max(origin.lng, dest.lng), ), ), - left: 40.0, + left: 40, top: mq.padding.top + 117.0, - right: 40.0, + right: 40, bottom: mq.padding.bottom + 224.0, ), ); @@ -131,7 +130,7 @@ class _MapScreenState extends ConsumerState { if (previous?.isLoading != next.isLoading && next.preview != null) { final overlay = _routeOverlay; if (overlay != null) { - await overlay.setDimmed(controller, next.isLoading); + await overlay.setDimmed(controller, dimmed: next.isLoading); } } @@ -142,8 +141,8 @@ class _MapScreenState extends ConsumerState { fitPadding = EdgeInsets.only( top: mq.padding.top + 117.0, bottom: mq.padding.bottom + 224.0, - left: 40.0, - right: 40.0, + left: 40, + right: 40, ); } @@ -169,7 +168,7 @@ class _MapScreenState extends ConsumerState { await controller.animateCamera( CameraUpdate.newLatLngZoom(LatLng(pos.latitude, pos.longitude), 16), ); - } catch (e) { + } on Object catch (e) { if (!mounted) return; final l10n = AppLocalizations.of(context)!; ScaffoldMessenger.of(context).showSnackBar( @@ -187,15 +186,15 @@ class _MapScreenState extends ConsumerState { if (ref.read(routeControllerProvider).origin == null) { final origin = await _resolveCurrentOriginLocation(l10n); if (!mounted) return; - notifier.setOrigin(origin); + unawaited(notifier.setOrigin(origin)); } - notifier.setDestination(Location( + unawaited(notifier.setDestination(Location( id: home.id, name: l10n.settingsHome, label: home.label, lng: home.lng, lat: home.lat, - )); + ))); } Future _updateHomeMarker(Location? home) async { @@ -241,12 +240,12 @@ class _MapScreenState extends ConsumerState { // Prefer the live MapLibre fix (same source as the blue dot, written by // onUserLocationUpdated). Fall back to Geolocator's cache only if MapLibre // hasn't emitted yet — its cache can lag behind by 20-30m at cycling speed. - UserLocation? initial = ref.read(userLocationProvider); + var initial = ref.read(userLocationProvider); if (initial == null) { try { final pos = await Geolocator.getLastKnownPosition(); if (pos != null) initial = positionToUserLocation(pos); - } catch (_) {} + } on Object catch (_) {} } try { await service.start( @@ -255,14 +254,14 @@ class _MapScreenState extends ConsumerState { WaypointInput(lat: destination.lat, lng: destination.lng), initialLocation: initial, ); - if (mounted) _speakNav(AppLocalizations.of(context)!.navTtsDeparting); + if (mounted) unawaited(_speakNav(AppLocalizations.of(context)!.navTtsDeparting)); if (initial != null) { // We already have a fix — skip awaitingFirstFix entirely. Without this // the camera waits for ferrostar to emit a `.navigating` state with // snapped_location, which won't happen until a stream tick arrives. await _activateFollowingCamera(initial); } - } catch (e, st) { + } on Object catch (e, st) { reportError(e, st, context: 'nav.start'); } } @@ -271,7 +270,7 @@ class _MapScreenState extends ConsumerState { if (!ref.read(ttsEnabledProvider)) return; try { await ref.read(flutterTtsProvider).speak(text); - } catch (e) { + } on Object catch (e) { debugPrint('nav: tts error: $e'); } } @@ -306,7 +305,7 @@ class _MapScreenState extends ConsumerState { final cam = ref.read(navigationCameraControllerProvider); if (cam.mode != CameraMode.awaitingFirstFix) return; debugPrint('nav: activating following camera'); - AppHaptics.firstFix(); + unawaited(AppHaptics.firstFix()); cam.onNavStart(); final controller = _mapController; if (controller == null) return; @@ -321,16 +320,14 @@ class _MapScreenState extends ConsumerState { Future _handleArrival() async { debugPrint('nav: arrived'); - AppHaptics.arrived(); - if (mounted) _speakNav(AppLocalizations.of(context)!.navTtsArrived); - final cam = ref.read(navigationCameraControllerProvider); - cam.onArrived(); + unawaited(AppHaptics.arrived()); + if (mounted) unawaited(_speakNav(AppLocalizations.of(context)!.navTtsArrived)); + ref.read(navigationCameraControllerProvider).onArrived(); if (mounted) setState(() => _rerouting = false); final controller = _mapController; if (controller == null) return; final destination = ref.read(routeControllerProvider).destination; - await controller - .updateMyLocationTrackingMode(MyLocationTrackingMode.none); + await controller.updateMyLocationTrackingMode(MyLocationTrackingMode.none); if (!mounted) return; if (destination != null) { await controller.animateCamera(CameraUpdate.newLatLngZoom( @@ -353,8 +350,8 @@ class _MapScreenState extends ConsumerState { final snapped = ref.read(navigationStateProvider).value?.snappedLocation; final loc = snapped ?? ref.read(userLocationProvider); if (loc == null) return; - final cam = ref.read(navigationCameraControllerProvider); - cam.onRecenterTapped(); + final cam = ref.read(navigationCameraControllerProvider) + ..onRecenterTapped(); await controller.animateCamera(CameraUpdate.newLatLngZoom( LatLng(loc.lat, loc.lng), cam.followZoom)); if (!mounted) return; @@ -372,7 +369,7 @@ class _MapScreenState extends ConsumerState { if (prevState?.status != TripStatus.complete && nextState.status == TripStatus.complete) { - _handleArrival(); + unawaited(_handleArrival()); } } @@ -391,27 +388,27 @@ class _MapScreenState extends ConsumerState { // isn't updated by ferrostar's replaceRoute. Re-hitting `/api/route` // with current GPS as origin produces a fresh GeoJSON preview. if (prevInProgress && !inProgress) { - _refreshPreviewFromGps(); + unawaited(_refreshPreviewFromGps()); } } Future _refreshPreviewFromGps() async { final cached = ref.read(userLocationProvider); - double? lat = cached?.lat; - double? lng = cached?.lng; + var lat = cached?.lat; + var lng = cached?.lng; if (lat == null || lng == null) { try { final pos = await Geolocator.getLastKnownPosition() ?? await Geolocator.getCurrentPosition(); lat = pos.latitude; lng = pos.longitude; - } catch (e) { + } on Object catch (e) { debugPrint('nav: refresh-preview GPS error: $e'); } } if (!mounted || lat == null || lng == null) return; final l10n = AppLocalizations.of(context)!; - ref.read(routeControllerProvider.notifier).setOrigin( + unawaited(ref.read(routeControllerProvider.notifier).setOrigin( Location( id: 'gps', name: l10n.locationCurrent, @@ -419,7 +416,7 @@ class _MapScreenState extends ConsumerState { lat: lat, lng: lng, ), - ); + )); } /// Resolves a "current location" Location for use as a route origin. Prefers @@ -440,7 +437,7 @@ class _MapScreenState extends ConsumerState { try { pos = await Geolocator.getLastKnownPosition() ?? await Geolocator.getCurrentPosition(); - } catch (_) {} + } on Object catch (_) {} return Location( id: 'gps', name: l10n.locationCurrent, @@ -469,7 +466,7 @@ class _MapScreenState extends ConsumerState { final id = props?['id']; if (id is! int || geom == null) return null; return TapFeature(areaId: id, geometry: geom); - } catch (_) { + } on Object catch (_) { return null; } } @@ -506,42 +503,43 @@ class _MapScreenState extends ConsumerState { routeState.error != null || preview != null; - ref.listen>(homeLocationProvider, (_, next) { - _updateHomeMarker(next.valueOrNull); - }); - ref.listen(ratingOverlayControllerProvider, - (prev, next) { - // One-shot toast: fires exactly when liveSyncDegraded flips false→true. - // Happens once per app session (either the client flag is off, or the - // server 404s the SSE endpoint on first connect). - if (prev?.liveSyncDegraded != true && next.liveSyncDegraded) { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text( - 'Live sync unavailable. Painted areas update on next app start.'), - ), - ); - } - }); - ref.listen(routeControllerProvider, _onRouteStateChanged); - ref.listen>( - navigationStateProvider, _onNavStateChange); - ref.listen>( - rerouteInProgressProvider, _onRerouteInProgressChange); - ref.listen>(rerouteSucceededProvider, (_, next) { - if (next is AsyncData) { - _speakNav(AppLocalizations.of(context)!.navTtsRerouted); - } - }); - ref.listen(navigationSessionProvider, (prev, next) { - if (prev == next) return; - if (next) { - _brushNotifier?.forceOff(); - _startNavigationSession(); - } else if (prev == true) { - _endNavigationSession(); - } - }); + ref + ..listen>(homeLocationProvider, (_, next) { + unawaited(_updateHomeMarker(next.valueOrNull)); + }) + ..listen(ratingOverlayControllerProvider, + (prev, next) { + // One-shot toast: fires exactly when liveSyncDegraded flips false→true. + // Happens once per app session (either the client flag is off, or the + // server 404s the SSE endpoint on first connect). + if (prev?.liveSyncDegraded != true && next.liveSyncDegraded) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text( + 'Live sync unavailable. Painted areas update on next app start.'), + ), + ); + } + }) + ..listen(routeControllerProvider, _onRouteStateChanged) + ..listen>( + navigationStateProvider, _onNavStateChange) + ..listen>( + rerouteInProgressProvider, _onRerouteInProgressChange) + ..listen>(rerouteSucceededProvider, (_, next) { + if (next is AsyncData) { + unawaited(_speakNav(AppLocalizations.of(context)!.navTtsRerouted)); + } + }) + ..listen(navigationSessionProvider, (prev, next) { + if (prev == next) return; + if (next) { + _brushNotifier?.forceOff(); + unawaited(_startNavigationSession()); + } else if (prev == true) { + unawaited(_endNavigationSession()); + } + }); return Scaffold( body: Stack( @@ -626,7 +624,6 @@ class _MapScreenState extends ConsumerState { cameraTargetBounds: CameraTargetBounds(_berlinBounds), minMaxZoomPreference: const MinMaxZoomPreference(10, 18), myLocationEnabled: true, - myLocationTrackingMode: MyLocationTrackingMode.none, trackCameraPosition: true, scrollGesturesEnabled: !paintMode, rotateGesturesEnabled: !paintMode, @@ -648,8 +645,8 @@ class _MapScreenState extends ConsumerState { gestureRecognizers: paintMode ? const >{} : >{ - Factory( - () => EagerGestureRecognizer(), + const Factory( + EagerGestureRecognizer.new, ), }, onMapCreated: (controller) { @@ -669,11 +666,12 @@ class _MapScreenState extends ConsumerState { await ref .read(ratingOverlayControllerProvider.notifier) .attachToMap(c); - _brushOverlay?.detach(); + final priorBrush = _brushOverlay; + if (priorBrush != null) unawaited(priorBrush.detach()); _brushOverlay = await BrushOverlay.attach(c); - _brushNotifier?.attach(surface: _brushOverlay!); - _updateHomeMarker( - ref.read(homeLocationProvider).valueOrNull); + _brushNotifier?.attach(_brushOverlay!); + unawaited(_updateHomeMarker( + ref.read(homeLocationProvider).valueOrNull)); }, onMapClick: _handleMapTap, onCameraTrackingDismissed: () { @@ -749,7 +747,7 @@ class _MapScreenState extends ConsumerState { onFlyToMyLocation: _flyToCurrentLocation, onResetBearing: _resetBearingToNorth, onStart: () { - AppHaptics.startRide(); + unawaited(AppHaptics.startRide()); ref .read(navigationSessionProvider.notifier) .state = true; @@ -837,7 +835,6 @@ class _UndoRedoFab extends StatelessWidget { child: Material( key: ValueKey(keyValue), shape: const CircleBorder(), - elevation: 0, color: bg, child: InkWell( customBorder: const CircleBorder(), @@ -880,12 +877,12 @@ class _PaintSheetWrapper extends StatelessWidget { @override Widget build(BuildContext context) { - return Align( + return const Align( alignment: Alignment.bottomCenter, child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.end, - children: const [ + children: [ Padding( padding: EdgeInsets.fromLTRB(0, 0, 16, 8), child: _PaintFabColumn(), @@ -942,17 +939,20 @@ class _PaintGestureWrap extends StatelessWidget { // Report positions from the initial pointer-down event, not // from where slop was resolved. Without this the brush stroke // jumps ~18px away from the finger on touch-down. - r.dragStartBehavior = DragStartBehavior.down; - r.onStart = (d) async { - final latLng = await toLatLng(_point(d.localPosition)); - onPanStart(latLng); - }; - r.onUpdate = (d) async { - final latLng = await toLatLng(_point(d.localPosition)); - onPanUpdate(latLng); - }; - r.onEnd = (_) => onPanEnd(); - r.onCancel = onPanCancel; + r + ..dragStartBehavior = DragStartBehavior.down + ..onStart = (d) async { + final latLng = await toLatLng(_point(d.localPosition)); + onPanStart(latLng); + } + ..onUpdate = (d) async { + final latLng = await toLatLng(_point(d.localPosition)); + onPanUpdate(latLng); + } + ..onEnd = (_) { + onPanEnd(); + } + ..onCancel = onPanCancel; }, ), LongPressGestureRecognizer: diff --git a/mobile/lib/screens/onboarding_screen.dart b/mobile/lib/screens/onboarding_screen.dart index b19db59..52fcf98 100644 --- a/mobile/lib/screens/onboarding_screen.dart +++ b/mobile/lib/screens/onboarding_screen.dart @@ -1,16 +1,17 @@ +import 'dart:async'; + +import 'package:beebeebike/l10n/generated/app_localizations.dart'; +import 'package:beebeebike/providers/onboarding_provider.dart'; +import 'package:beebeebike/screens/login_screen.dart'; +import 'package:beebeebike/screens/privacy_policy_screen.dart'; +import 'package:beebeebike/screens/register_screen.dart'; +import 'package:beebeebike/theme/tokens.dart'; +import 'package:beebeebike/theme/typography.dart'; +import 'package:beebeebike/widgets/onboarding_dots.dart'; +import 'package:beebeebike/widgets/onboarding_page.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import '../l10n/generated/app_localizations.dart'; -import '../providers/onboarding_provider.dart'; -import '../theme/tokens.dart'; -import '../theme/typography.dart'; -import '../widgets/onboarding_dots.dart'; -import '../widgets/onboarding_page.dart'; -import 'login_screen.dart'; -import 'privacy_policy_screen.dart'; -import 'register_screen.dart'; - class OnboardingScreen extends ConsumerStatefulWidget { const OnboardingScreen({super.key}); @@ -31,17 +32,17 @@ class _OnboardingScreenState extends ConsumerState { } void _goNext() { - _controller.nextPage( + unawaited(_controller.nextPage( duration: const Duration(milliseconds: 240), curve: Curves.easeOut, - ); + )); } void _goBack() { - _controller.previousPage( + unawaited(_controller.previousPage( duration: const Duration(milliseconds: 240), curve: Curves.easeOut, - ); + )); } Future _complete() async { @@ -49,25 +50,25 @@ class _OnboardingScreenState extends ConsumerState { } void _openRegister() { - Navigator.of(context).push( - MaterialPageRoute( + unawaited(Navigator.of(context).push( + MaterialPageRoute( builder: (_) => RegisterScreen(onSuccess: _complete), ), - ); + )); } void _openLogin() { - Navigator.of(context).push( - MaterialPageRoute( + unawaited(Navigator.of(context).push( + MaterialPageRoute( builder: (_) => LoginScreen(onSuccess: _complete), ), - ); + )); } void _openPrivacyPolicy() { - Navigator.of(context).push( - MaterialPageRoute(builder: (_) => const PrivacyPolicyScreen()), - ); + unawaited(Navigator.of(context).push( + MaterialPageRoute(builder: (_) => const PrivacyPolicyScreen()), + )); } String _eyebrow(int i) => '0${i + 1} / 0$_pageCount'; diff --git a/mobile/lib/screens/privacy_policy_screen.dart b/mobile/lib/screens/privacy_policy_screen.dart index 529232f..4f4bf5d 100644 --- a/mobile/lib/screens/privacy_policy_screen.dart +++ b/mobile/lib/screens/privacy_policy_screen.dart @@ -1,10 +1,9 @@ +import 'package:beebeebike/app.dart'; +import 'package:beebeebike/l10n/generated/app_localizations.dart'; +import 'package:beebeebike/screens/legal_document_screen.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import '../app.dart'; -import '../l10n/generated/app_localizations.dart'; -import 'legal_document_screen.dart'; - class PrivacyPolicyScreen extends ConsumerWidget { const PrivacyPolicyScreen({super.key}); diff --git a/mobile/lib/screens/register_screen.dart b/mobile/lib/screens/register_screen.dart index 260cf57..a8e0ac8 100644 --- a/mobile/lib/screens/register_screen.dart +++ b/mobile/lib/screens/register_screen.dart @@ -1,8 +1,7 @@ +import 'package:beebeebike/l10n/generated/app_localizations.dart'; +import 'package:beebeebike/widgets/register_form.dart'; import 'package:flutter/material.dart'; -import '../l10n/generated/app_localizations.dart'; -import '../widgets/register_form.dart'; - class RegisterScreen extends StatelessWidget { const RegisterScreen({super.key, this.onSuccess}); diff --git a/mobile/lib/screens/search_screen.dart b/mobile/lib/screens/search_screen.dart index 902b760..b9374f4 100644 --- a/mobile/lib/screens/search_screen.dart +++ b/mobile/lib/screens/search_screen.dart @@ -1,18 +1,17 @@ import 'dart:async'; +import 'package:beebeebike/api/client.dart'; +import 'package:beebeebike/api/geocode_api.dart'; +import 'package:beebeebike/l10n/generated/app_localizations.dart'; +import 'package:beebeebike/models/geocode_result.dart'; +import 'package:beebeebike/models/location.dart'; +import 'package:beebeebike/providers/location_provider.dart'; +import 'package:beebeebike/providers/search_history_provider.dart'; +import 'package:beebeebike/theme/tokens.dart'; +import 'package:beebeebike/theme/typography.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import '../api/client.dart'; -import '../api/geocode_api.dart'; -import '../l10n/generated/app_localizations.dart'; -import '../models/geocode_result.dart'; -import '../models/location.dart'; -import '../providers/location_provider.dart'; -import '../providers/search_history_provider.dart'; -import '../theme/tokens.dart'; -import '../theme/typography.dart'; - final _geocodeApiProvider = Provider((ref) => GeocodeApi(ref.watch(dioProvider))); @@ -39,7 +38,7 @@ class _SearchScreenState extends ConsumerState { void _onChanged(String value) { _debounce?.cancel(); if (value.trim().isEmpty) { - setState(() => _results.clear()); + setState(_results.clear); return; } _debounce = Timer( @@ -53,8 +52,8 @@ class _SearchScreenState extends ConsumerState { try { final results = await ref.read(_geocodeApiProvider).search(query); if (mounted) setState(() => _results..clear()..addAll(results)); - } catch (_) { - if (mounted) setState(() => _results.clear()); + } on Object catch (_) { + if (mounted) setState(_results.clear); } finally { if (mounted) setState(() => _loading = false); } @@ -91,7 +90,7 @@ class _SearchScreenState extends ConsumerState { onChanged: _onChanged, onSubmitted: (value) { _debounce?.cancel(); - if (value.trim().isNotEmpty) _search(value.trim()); + if (value.trim().isNotEmpty) unawaited(_search(value.trim())); }, ), ), @@ -204,8 +203,7 @@ class _SearchRow extends StatelessWidget { const _SearchRow({ required this.icon, required this.title, - this.subtitle, - required this.onTap, + required this.onTap, this.subtitle, }); final IconData icon; diff --git a/mobile/lib/screens/settings_screen.dart b/mobile/lib/screens/settings_screen.dart index 7f6f670..90e139b 100644 --- a/mobile/lib/screens/settings_screen.dart +++ b/mobile/lib/screens/settings_screen.dart @@ -1,20 +1,19 @@ +import 'package:beebeebike/app.dart'; +import 'package:beebeebike/l10n/generated/app_localizations.dart'; +import 'package:beebeebike/providers/auth_provider.dart'; +import 'package:beebeebike/providers/location_provider.dart'; +import 'package:beebeebike/screens/legal_document_screen.dart'; +import 'package:beebeebike/screens/login_screen.dart'; +import 'package:beebeebike/screens/register_screen.dart'; +import 'package:beebeebike/services/error_reporter.dart'; +import 'package:beebeebike/theme/tokens.dart'; +import 'package:beebeebike/theme/typography.dart'; +import 'package:beebeebike/widgets/language_picker.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:url_launcher/url_launcher.dart'; -import '../app.dart'; -import '../l10n/generated/app_localizations.dart'; -import '../providers/auth_provider.dart'; -import '../providers/location_provider.dart'; -import '../services/error_reporter.dart'; -import '../theme/tokens.dart'; -import '../theme/typography.dart'; -import '../widgets/language_picker.dart'; -import 'legal_document_screen.dart'; -import 'login_screen.dart'; -import 'register_screen.dart'; - class SettingsScreen extends ConsumerWidget { const SettingsScreen({super.key}); @@ -154,8 +153,8 @@ class _AuthButton extends ConsumerWidget { label: l10n.onboardingCreateAccount, bg: BbbColors.ink, fg: Colors.white, - onTap: () => Navigator.of(context).push( - MaterialPageRoute(builder: (_) => const RegisterScreen()), + onTap: () => Navigator.of(context).push( + MaterialPageRoute(builder: (_) => const RegisterScreen()), ), ), const SizedBox(height: 8), @@ -164,8 +163,8 @@ class _AuthButton extends ConsumerWidget { label: l10n.settingsLogIn, bg: BbbColors.bgAlt, fg: BbbColors.ink, - onTap: () => Navigator.of(context).push( - MaterialPageRoute(builder: (_) => const LoginScreen()), + onTap: () => Navigator.of(context).push( + MaterialPageRoute(builder: (_) => const LoginScreen()), ), ), ], @@ -175,11 +174,7 @@ class _AuthButton extends ConsumerWidget { class _SettingsAuthTile extends StatelessWidget { const _SettingsAuthTile({ - super.key, - required this.label, - required this.bg, - required this.fg, - required this.onTap, + required this.label, required this.bg, required this.fg, required this.onTap, super.key, }); final String label; @@ -268,8 +263,8 @@ class _LegalTile extends StatelessWidget { @override Widget build(BuildContext context) { return InkWell( - onTap: () => Navigator.of(context).push( - MaterialPageRoute( + onTap: () => Navigator.of(context).push( + MaterialPageRoute( builder: (_) => LegalDocumentScreen(title: title, url: url), ), ), @@ -300,7 +295,7 @@ class _DangerSection extends ConsumerWidget { final isRegistered = user?.accountType == 'registered'; if (!isRegistered) return const SizedBox.shrink(); - final danger = BbbColors.rampHate; + const danger = BbbColors.rampHate; return Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -323,7 +318,7 @@ class _DangerSection extends ConsumerWidget { child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Icon(Icons.delete_forever_outlined, color: danger), + const Icon(Icons.delete_forever_outlined, color: danger), const SizedBox(width: 12), Expanded( child: Column( @@ -354,7 +349,7 @@ class _DangerSection extends ConsumerWidget { Future _confirmDelete(BuildContext context, WidgetRef ref) async { final l10n = AppLocalizations.of(context)!; - final danger = BbbColors.rampHate; + const danger = BbbColors.rampHate; final confirmed = await showDialog( context: context, builder: (dialogContext) => AlertDialog( @@ -384,7 +379,7 @@ class _DangerSection extends ConsumerWidget { SnackBar(content: Text(l10n.settingsDeleteSuccess)), ); if (navigator.canPop()) navigator.pop(); - } catch (error, stackTrace) { + } on Object catch (error, stackTrace) { reportError(error, stackTrace, context: 'settings.deleteAccount'); messenger.showSnackBar( SnackBar( diff --git a/mobile/lib/services/brush_geometry.dart b/mobile/lib/services/brush_geometry.dart index 401155a..ca0f982 100644 --- a/mobile/lib/services/brush_geometry.dart +++ b/mobile/lib/services/brush_geometry.dart @@ -12,8 +12,8 @@ import 'package:maplibre_gl/maplibre_gl.dart'; class BrushGeometry { BrushGeometry._(); - static const double minMovePx = 4.0; - static const double brushPx = 27.0; + static const double minMovePx = 4; + static const double brushPx = 27; static const double minRadiusKm = 0.005; static double metersPerPixel({required double lat, required double zoom}) { @@ -46,7 +46,8 @@ class BrushGeometry { final mPerDegLon = 111320.0 * math.cos(avgLat * math.pi / 180); final path = []; - double? lastX, lastY; + double? lastX; + double? lastY; for (final p in points) { final x = (p.longitude - p0.longitude) * mPerDegLon; final y = (p.latitude - p0.latitude) * mPerDegLat; diff --git a/mobile/lib/services/brush_overlay.dart b/mobile/lib/services/brush_overlay.dart index f81ba38..8241632 100644 --- a/mobile/lib/services/brush_overlay.dart +++ b/mobile/lib/services/brush_overlay.dart @@ -54,7 +54,7 @@ class BrushOverlay implements BrushOverlaySurface { await controller.addGeoJsonSource( sourceId, - const {'type': 'FeatureCollection', 'features': []}, + const {'type': 'FeatureCollection', 'features': []}, ); await controller.addFillLayer( sourceId, @@ -74,17 +74,17 @@ class BrushOverlay implements BrushOverlaySurface { /// /// Since [setPreview] is called on every pointer-move event, the geometry always /// changes but the color usually stays the same within a stroke. This method skips - /// [setLayerProperties] calls when the color hex hasn't changed. + /// `setLayerProperties` calls when the color hex hasn't changed. @override Future setPreview( Map geometry, String colorHex, ) async { if (!_attached) return; - await _controller.setGeoJsonSource(sourceId, { + await _controller.setGeoJsonSource(sourceId, { 'type': 'FeatureCollection', - 'features': [ - {'type': 'Feature', 'properties': {}, 'geometry': geometry}, + 'features': >[ + {'type': 'Feature', 'properties': {}, 'geometry': geometry}, ], }); if (colorHex != _lastColorHex) { @@ -102,9 +102,9 @@ class BrushOverlay implements BrushOverlaySurface { @override Future clear() async { if (!_attached) return; - await _controller.setGeoJsonSource(sourceId, const { + await _controller.setGeoJsonSource(sourceId, const { 'type': 'FeatureCollection', - 'features': [], + 'features': [], }); } @@ -117,9 +117,9 @@ class BrushOverlay implements BrushOverlaySurface { _attached = false; try { await _controller.removeLayer(fillLayerId); - } catch (_) {} + } on Object catch (_) {} try { await _controller.removeSource(sourceId); - } catch (_) {} + } on Object catch (_) {} } } diff --git a/mobile/lib/services/error_reporter.dart b/mobile/lib/services/error_reporter.dart index ddc1f43..0445ab4 100644 --- a/mobile/lib/services/error_reporter.dart +++ b/mobile/lib/services/error_reporter.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:flutter/foundation.dart'; import 'package:sentry_flutter/sentry_flutter.dart'; @@ -16,11 +18,11 @@ void reportError( if (kDebugMode) { debugPrint('$context error: $error\n$stackTrace'); } - Sentry.captureException( + unawaited(Sentry.captureException( error, stackTrace: stackTrace, withScope: (scope) => scope.setTag('context', context), - ); + )); } /// Record a breadcrumb attached to subsequent Sentry events, and mirror it to @@ -29,7 +31,7 @@ void reportError( /// lands without becoming their own GlitchTip issues. void addBreadcrumb(String message, {String? category}) { if (kDebugMode) debugPrint('${category ?? "breadcrumb"}: $message'); - Sentry.addBreadcrumb( + unawaited(Sentry.addBreadcrumb( Breadcrumb(message: message, category: category, level: SentryLevel.info), - ); + )); } diff --git a/mobile/lib/services/haptics.dart b/mobile/lib/services/haptics.dart index 9c49f57..3e2975b 100644 --- a/mobile/lib/services/haptics.dart +++ b/mobile/lib/services/haptics.dart @@ -15,7 +15,7 @@ class AppHaptics { if (await hf.Haptics.canVibrate()) { await hf.Haptics.vibrate(type); } - } catch (_) { + } on Object catch (_) { // Haptics are fire-and-forget; failure must never break app flow. } } diff --git a/mobile/lib/services/home_marker_service.dart b/mobile/lib/services/home_marker_service.dart index 62c3967..785d57c 100644 --- a/mobile/lib/services/home_marker_service.dart +++ b/mobile/lib/services/home_marker_service.dart @@ -1,11 +1,10 @@ import 'dart:ui' as ui; -import 'package:flutter/material.dart'; +import 'package:beebeebike/models/location.dart'; import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; import 'package:maplibre_gl/maplibre_gl.dart'; -import '../models/location.dart'; - /// Owns the home-location Symbol on the MapLibre map. Re-renders the marker /// image and places a single [Symbol] at [Location], or removes it when home /// is null. @@ -28,7 +27,7 @@ class HomeMarkerService { _marker = await controller.addSymbol(SymbolOptions( geometry: LatLng(home.lat, home.lng), iconImage: 'home-marker', - iconSize: 1.0, + iconSize: 1, iconAnchor: 'center', )); } @@ -36,12 +35,12 @@ class HomeMarkerService { static Future _createImage() async { const double size = 52; final recorder = ui.PictureRecorder(); - final canvas = Canvas(recorder); - canvas.drawCircle( - const Offset(size / 2, size / 2), - size / 2, - Paint()..color = const Color(0xFF3B82F6), - ); + final canvas = Canvas(recorder) + ..drawCircle( + const Offset(size / 2, size / 2), + size / 2, + Paint()..color = const Color(0xFF3B82F6), + ); final tp = TextPainter(textDirection: TextDirection.ltr) ..text = TextSpan( text: String.fromCharCode(Icons.home.codePoint), diff --git a/mobile/lib/services/map_style_loader.dart b/mobile/lib/services/map_style_loader.dart index 9a18485..7664105 100644 --- a/mobile/lib/services/map_style_loader.dart +++ b/mobile/lib/services/map_style_loader.dart @@ -1,11 +1,10 @@ import 'dart:io'; +import 'package:beebeebike/app.dart'; import 'package:flutter/services.dart' show rootBundle; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:path_provider/path_provider.dart'; -import '../app.dart'; - const _styleAssetPath = 'assets/styles/beebeebike-style.json'; /// Loads the bundled style, substitutes the tile-server base URL, and writes diff --git a/mobile/lib/services/rating_events_client.dart b/mobile/lib/services/rating_events_client.dart index dcacb52..20b947f 100644 --- a/mobile/lib/services/rating_events_client.dart +++ b/mobile/lib/services/rating_events_client.dart @@ -1,12 +1,11 @@ import 'dart:async'; import 'dart:convert'; +import 'package:beebeebike/api/client.dart'; import 'package:dio/dio.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import '../api/client.dart'; - /// Opens a chunked-byte stream for the SSE endpoint. Split out so tests can /// inject a fake without touching Dio or real sockets. The client must be /// able to cancel an in-flight open via [cancelToken] and to notice stream @@ -65,11 +64,9 @@ Stream parseSseStream(Stream> bytes) async* { switch (field) { case 'event': eventType = value; - break; case 'data': if (dataBuf.isNotEmpty) dataBuf.write('\n'); dataBuf.write(value); - break; default: break; // id / retry / unknown fields } @@ -189,13 +186,13 @@ class RatingEventsClient { _serverDisabled = true; try { onServerDisabled?.call(); - } catch (e) { + } on Object catch (e) { _log('rating-events: onServerDisabled threw: $e'); } return; } _log('rating-events: connect failed: ${e.message}'); - } catch (e) { + } on Object catch (e) { if (_stopped) return; _log('rating-events: connect failed: $e'); } @@ -210,7 +207,7 @@ class RatingEventsClient { // redundant refetch is cheap insurance. try { onInvalidate(); - } catch (e) { + } on Object catch (e) { _log('rating-events: onInvalidate threw on connect: $e'); } await _consume(bytes); @@ -241,7 +238,7 @@ class RatingEventsClient { if (ev.event == 'invalidate') { try { onInvalidate(); - } catch (e) { + } on Object catch (e) { _log('rating-events: onInvalidate threw: $e'); } } diff --git a/mobile/lib/services/rating_overlay.dart b/mobile/lib/services/rating_overlay.dart index 682d915..81ee313 100644 --- a/mobile/lib/services/rating_overlay.dart +++ b/mobile/lib/services/rating_overlay.dart @@ -55,7 +55,7 @@ class RatingOverlay implements RatingOverlaySurface { await controller.addGeoJsonSource( sourceId, - const {'type': 'FeatureCollection', 'features': []}, + const {'type': 'FeatureCollection', 'features': []}, ); final colorExpr = _matchExpression(); @@ -96,9 +96,9 @@ class RatingOverlay implements RatingOverlaySurface { /// Clear all rendered polygons without detaching the layers. @override - Future clear() => update(const { + Future clear() => update(const { 'type': 'FeatureCollection', - 'features': [], + 'features': [], }); /// Remove layers + source. Safe to call multiple times. @@ -108,13 +108,13 @@ class RatingOverlay implements RatingOverlaySurface { _attached = false; try { await _controller.removeLayer(lineLayerId); - } catch (_) {} + } on Object catch (_) {} try { await _controller.removeLayer(fillLayerId); - } catch (_) {} + } on Object catch (_) {} try { await _controller.removeSource(sourceId); - } catch (_) {} + } on Object catch (_) {} } /// Build a MapLibre `match` expression on the `value` property of each @@ -127,8 +127,9 @@ class RatingOverlay implements RatingOverlaySurface { ['get', 'value'], ]; for (final entry in _colors.entries) { - expr.add(entry.key); - expr.add(entry.value); + expr + ..add(entry.key) + ..add(entry.value); } expr.add(_fallbackColor); return expr; diff --git a/mobile/lib/services/route_drawing.dart b/mobile/lib/services/route_drawing.dart index 07bd5c4..27864be 100644 --- a/mobile/lib/services/route_drawing.dart +++ b/mobile/lib/services/route_drawing.dart @@ -1,8 +1,7 @@ +import 'package:beebeebike/models/route_preview.dart'; import 'package:flutter/material.dart'; import 'package:maplibre_gl/maplibre_gl.dart'; -import '../models/route_preview.dart'; - const _routeLineColor = '#19A4C2'; const _markerFillColor = '#14272F'; const _markerStrokeColor = '#ffffff'; @@ -51,22 +50,22 @@ class RouteOverlay { final line = await controller.addLine(LineOptions( geometry: coords, lineColor: _routeLineColor, - lineWidth: 8.0, + lineWidth: 8, lineOpacity: _lineOpacityFull, )); final origin = await controller.addCircle(CircleOptions( geometry: coords.first, - circleRadius: 8.0, + circleRadius: 8, circleColor: _markerFillColor, circleStrokeColor: _markerStrokeColor, - circleStrokeWidth: 2.0, + circleStrokeWidth: 2, )); final destination = await controller.addCircle(CircleOptions( geometry: coords.last, - circleRadius: 8.0, + circleRadius: 8, circleColor: _markerFillColor, circleStrokeColor: _markerStrokeColor, - circleStrokeWidth: 2.0, + circleStrokeWidth: 2, )); if (fitPadding != null) { await controller.animateCamera( @@ -89,9 +88,9 @@ class RouteOverlay { } Future setDimmed( - MapLibreMapController controller, - bool dimmed, - ) async { + MapLibreMapController controller, { + required bool dimmed, + }) async { await controller.updateLine( _line, LineOptions( diff --git a/mobile/lib/theme/app_theme.dart b/mobile/lib/theme/app_theme.dart index 00449ee..a995300 100644 --- a/mobile/lib/theme/app_theme.dart +++ b/mobile/lib/theme/app_theme.dart @@ -1,13 +1,11 @@ +import 'package:beebeebike/theme/tokens.dart'; import 'package:flutter/material.dart'; import 'package:google_fonts/google_fonts.dart'; -import 'tokens.dart'; - ThemeData buildBbbTheme() { final base = ThemeData( colorScheme: ColorScheme.fromSeed( seedColor: BbbColors.brand, - brightness: Brightness.light, primary: BbbColors.brand, surface: BbbColors.panel, onSurface: BbbColors.ink, diff --git a/mobile/lib/theme/typography.dart b/mobile/lib/theme/typography.dart index 73aab6b..e21eeb7 100644 --- a/mobile/lib/theme/typography.dart +++ b/mobile/lib/theme/typography.dart @@ -1,8 +1,7 @@ +import 'package:beebeebike/theme/tokens.dart'; import 'package:flutter/material.dart'; import 'package:google_fonts/google_fonts.dart'; -import 'tokens.dart'; - /// Type roles per style guide. Manrope for UI, JetBrains Mono for mono sub / /// stats / eyebrow microcopy. Fraunces is display-only and not used on the /// landing screen, so it is not wired here. @@ -67,6 +66,6 @@ class BbbText { fontWeight: FontWeight.w700, letterSpacing: -1.5, color: color, - height: 1.0, + height: 1, ); } diff --git a/mobile/lib/widgets/arrived_sheet.dart b/mobile/lib/widgets/arrived_sheet.dart index 351c57e..2dae1e1 100644 --- a/mobile/lib/widgets/arrived_sheet.dart +++ b/mobile/lib/widgets/arrived_sheet.dart @@ -1,11 +1,10 @@ +import 'package:beebeebike/l10n/generated/app_localizations.dart'; +import 'package:beebeebike/theme/tokens.dart'; +import 'package:beebeebike/theme/typography.dart'; import 'package:flutter/material.dart'; -import '../l10n/generated/app_localizations.dart'; -import '../theme/tokens.dart'; -import '../theme/typography.dart'; - class ArrivedSheet extends StatelessWidget { - const ArrivedSheet({super.key, required this.onDone}); + const ArrivedSheet({required this.onDone, super.key}); final VoidCallback onDone; diff --git a/mobile/lib/widgets/brush_fab.dart b/mobile/lib/widgets/brush_fab.dart index abeb6f3..19bd6c6 100644 --- a/mobile/lib/widgets/brush_fab.dart +++ b/mobile/lib/widgets/brush_fab.dart @@ -1,13 +1,12 @@ +import 'package:beebeebike/l10n/generated/app_localizations.dart'; +import 'package:beebeebike/providers/brush_provider.dart'; +import 'package:beebeebike/theme/tokens.dart'; +import 'package:beebeebike/widgets/paint_roller_icon.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import '../l10n/generated/app_localizations.dart'; -import '../providers/brush_provider.dart'; -import '../theme/tokens.dart'; -import 'paint_roller_icon.dart'; - /// Always-visible paint-mode toggle. Sits at the bottom of the right-side -/// FAB column on every non-navigation view. Flips [BrushController.paintMode]. +/// FAB column on every non-navigation view. Flips `BrushController.paintMode`. class BrushFab extends ConsumerWidget { const BrushFab({super.key}); @@ -30,10 +29,8 @@ class BrushFab extends ConsumerWidget { shape: CircleBorder( side: BorderSide( color: active ? BbbColors.ink : BbbColors.divider, - width: 1, ), ), - elevation: 0, child: InkWell( customBorder: const CircleBorder(), onTap: () => @@ -48,7 +45,6 @@ class BrushFab extends ConsumerWidget { ), child: Center( child: PaintRollerIcon( - size: 24, color: active ? BbbColors.panel : BbbColors.ink, ), ), diff --git a/mobile/lib/widgets/eta_sheet.dart b/mobile/lib/widgets/eta_sheet.dart index 347a101..3ae47b2 100644 --- a/mobile/lib/widgets/eta_sheet.dart +++ b/mobile/lib/widgets/eta_sheet.dart @@ -1,13 +1,12 @@ +import 'package:beebeebike/l10n/generated/app_localizations.dart'; +import 'package:beebeebike/navigation/maneuver_icons.dart'; +import 'package:beebeebike/providers/route_provider.dart'; +import 'package:beebeebike/theme/tokens.dart'; +import 'package:beebeebike/theme/typography.dart'; import 'package:ferrostar_flutter/ferrostar_flutter.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import '../l10n/generated/app_localizations.dart'; -import '../navigation/maneuver_icons.dart'; -import '../providers/route_provider.dart'; -import '../theme/tokens.dart'; -import '../theme/typography.dart'; - /// Distance (m) within which, if the destination has a house number, /// the ETA hero swaps from "$mins min" to the street + house number so /// the rider sees exactly where to stop. @@ -18,9 +17,7 @@ const double _arrivalAddressThresholdM = 200; /// mono data strip with remaining distance + arrival clock. class EtaSheet extends ConsumerWidget { const EtaSheet({ - super.key, - required this.navState, - required this.onClose, + required this.navState, required this.onClose, super.key, }); final AsyncValue navState; @@ -53,12 +50,12 @@ class EtaSheet extends ConsumerWidget { Expanded( child: navState.when( loading: () => Text(l10n.commonLoading, style: BbbText.body()), - error: (_, __) => Text('—', style: BbbText.body()), + error: (_, _) => Text('—', style: BbbText.body()), data: (state) { final p = state.progress; if (p == null) return Text('—', style: BbbText.body()); final heroStyle = - BbbText.navHero(color: BbbColors.inkMuted); + BbbText.navHero(); if (addressLabel != null && p.distanceRemainingM < _arrivalAddressThresholdM) { return Text( @@ -85,7 +82,7 @@ class EtaSheet extends ConsumerWidget { const SizedBox(height: 6), navState.when( loading: () => const SizedBox.shrink(), - error: (_, __) => const SizedBox.shrink(), + error: (_, _) => const SizedBox.shrink(), data: (state) { final p = state.progress; if (p == null) return const SizedBox.shrink(); @@ -95,7 +92,7 @@ class EtaSheet extends ConsumerWidget { children: [ Text( distText, - style: BbbText.monoTime(color: BbbColors.inkMuted).copyWith( + style: BbbText.monoTime().copyWith( fontWeight: FontWeight.w600, ), ), @@ -105,7 +102,7 @@ class EtaSheet extends ConsumerWidget { ), Text( l10n.navArrives(arrival), - style: BbbText.monoTime(color: BbbColors.inkMuted), + style: BbbText.monoTime(), ), ], ); diff --git a/mobile/lib/widgets/home_sheet.dart b/mobile/lib/widgets/home_sheet.dart index b1cbc15..db5c4e0 100644 --- a/mobile/lib/widgets/home_sheet.dart +++ b/mobile/lib/widgets/home_sheet.dart @@ -1,28 +1,27 @@ +import 'dart:async'; + +import 'package:beebeebike/l10n/generated/app_localizations.dart'; +import 'package:beebeebike/models/location.dart'; +import 'package:beebeebike/providers/auth_provider.dart'; +import 'package:beebeebike/providers/home_eta_provider.dart'; +import 'package:beebeebike/providers/location_provider.dart'; +import 'package:beebeebike/providers/route_provider.dart'; +import 'package:beebeebike/providers/search_history_provider.dart'; +import 'package:beebeebike/screens/login_screen.dart'; +import 'package:beebeebike/screens/register_screen.dart'; +import 'package:beebeebike/theme/tokens.dart'; +import 'package:beebeebike/theme/typography.dart'; +import 'package:beebeebike/widgets/saved_item.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:geolocator/geolocator.dart'; -import '../l10n/generated/app_localizations.dart'; -import '../models/location.dart'; -import '../providers/auth_provider.dart'; -import '../providers/home_eta_provider.dart'; -import '../providers/location_provider.dart'; -import '../providers/route_provider.dart'; -import '../providers/search_history_provider.dart'; -import '../screens/login_screen.dart'; -import '../screens/register_screen.dart'; -import '../theme/tokens.dart'; -import '../theme/typography.dart'; -import 'saved_item.dart'; - /// Landing-state bottom sheet. Two snap points: peek (~16 %) and a mid /// stop sized to fit the Go Home row + three recent items. The paint /// brush FAB lives in the map-screen overlay column, not here. class HomeSheet extends ConsumerStatefulWidget { const HomeSheet({ - super.key, - required this.onNavigateHome, - required this.sheetController, + required this.onNavigateHome, required this.sheetController, super.key, }); final VoidCallback onNavigateHome; @@ -135,8 +134,8 @@ class _AuthRow extends StatelessWidget { bg: BbbColors.ink, fg: Colors.white, iconBg: const Color.fromRGBO(255, 255, 255, 0.14), - onTap: () => Navigator.of(context).push( - MaterialPageRoute(builder: (_) => const RegisterScreen()), + onTap: () => Navigator.of(context).push( + MaterialPageRoute(builder: (_) => const RegisterScreen()), ), ), ), @@ -149,8 +148,8 @@ class _AuthRow extends StatelessWidget { bg: BbbColors.bgAlt, fg: BbbColors.ink, iconBg: BbbColors.divider, - onTap: () => Navigator.of(context).push( - MaterialPageRoute(builder: (_) => const LoginScreen()), + onTap: () => Navigator.of(context).push( + MaterialPageRoute(builder: (_) => const LoginScreen()), ), ), ), @@ -161,13 +160,7 @@ class _AuthRow extends StatelessWidget { class _AuthButton extends StatelessWidget { const _AuthButton({ - super.key, - required this.label, - required this.icon, - required this.bg, - required this.fg, - required this.iconBg, - required this.onTap, + required this.label, required this.icon, required this.bg, required this.fg, required this.iconBg, required this.onTap, super.key, }); final String label; @@ -232,7 +225,7 @@ class _GoHomeButton extends StatelessWidget { if (eta == null) return null; return eta.when( loading: () => 'Calculating ETA…', - error: (_, __) => '—', + error: (_, _) => '—', data: (mins) => mins == null ? '—' : '$mins min', ); } @@ -348,7 +341,7 @@ class _RecentSectionState extends ConsumerState<_RecentSection> { if (!widget.sheetController.isAttached) return; if (widget.sheetController.size <= _pullThreshold) return; _distancesRequested = true; - _computeDistances(); + unawaited(_computeDistances()); } Future _computeDistances() async { @@ -358,7 +351,7 @@ class _RecentSectionState extends ConsumerState<_RecentSection> { try { pos = await Geolocator.getLastKnownPosition() ?? await Geolocator.getCurrentPosition(); - } catch (_) { + } on Object catch (_) { return; } if (!mounted) return; @@ -387,17 +380,17 @@ class _RecentSectionState extends ConsumerState<_RecentSection> { try { pos = await Geolocator.getLastKnownPosition() ?? await Geolocator.getCurrentPosition(); - } catch (_) {} + } on Object catch (_) {} if (!mounted) return; - notifier.setOrigin(Location( + unawaited(notifier.setOrigin(Location( id: 'gps', name: 'Mein Standort', label: 'Mein Standort', lng: pos?.longitude ?? 13.4533, lat: pos?.latitude ?? 52.5065, - )); + ))); } - notifier.setDestination(destination); + unawaited(notifier.setDestination(destination)); } @override @@ -428,4 +421,3 @@ class _RecentSectionState extends ConsumerState<_RecentSection> { ); } } - diff --git a/mobile/lib/widgets/language_picker.dart b/mobile/lib/widgets/language_picker.dart index 567fedf..6b950a8 100644 --- a/mobile/lib/widgets/language_picker.dart +++ b/mobile/lib/widgets/language_picker.dart @@ -1,10 +1,11 @@ +import 'dart:async'; + +import 'package:beebeebike/l10n/generated/app_localizations.dart'; +import 'package:beebeebike/providers/locale_provider.dart'; +import 'package:beebeebike/theme/typography.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import '../l10n/generated/app_localizations.dart'; -import '../providers/locale_provider.dart'; -import '../theme/typography.dart'; - class LanguagePicker extends ConsumerWidget { const LanguagePicker({super.key}); @@ -17,7 +18,7 @@ class LanguagePicker extends ConsumerWidget { return RadioGroup( groupValue: current, onChanged: (v) { - if (v != null) controller.setPref(v); + if (v != null) unawaited(controller.setPref(v)); }, child: Column( children: [ diff --git a/mobile/lib/widgets/login_form.dart b/mobile/lib/widgets/login_form.dart index f53598b..79269a9 100644 --- a/mobile/lib/widgets/login_form.dart +++ b/mobile/lib/widgets/login_form.dart @@ -1,10 +1,9 @@ +import 'package:beebeebike/l10n/generated/app_localizations.dart'; +import 'package:beebeebike/providers/auth_provider.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import '../l10n/generated/app_localizations.dart'; -import '../providers/auth_provider.dart'; - enum _LoginErrorKind { invalidCredentials } class LoginForm extends ConsumerStatefulWidget { diff --git a/mobile/lib/widgets/map/compass_fab_inline.dart b/mobile/lib/widgets/map/compass_fab_inline.dart index b033d06..0c79320 100644 --- a/mobile/lib/widgets/map/compass_fab_inline.dart +++ b/mobile/lib/widgets/map/compass_fab_inline.dart @@ -1,18 +1,17 @@ import 'dart:math' as math; +import 'package:beebeebike/l10n/generated/app_localizations.dart'; +import 'package:beebeebike/providers/map_bearing_provider.dart'; +import 'package:beebeebike/theme/tokens.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import '../../l10n/generated/app_localizations.dart'; -import '../../providers/map_bearing_provider.dart'; -import '../../theme/tokens.dart'; - /// Inline compass that sits above a RecenterFab in each sheet. Reads /// the map bearing from [mapBearingProvider] and renders nothing when /// the map is ~north-up; otherwise shows a rotated glyph + spacer, /// tapping animates the map back to bearing 0. class CompassFabInline extends ConsumerWidget { - const CompassFabInline({super.key, required this.onResetBearing}); + const CompassFabInline({required this.onResetBearing, super.key}); final VoidCallback onResetBearing; @@ -28,7 +27,6 @@ class CompassFabInline extends ConsumerWidget { child: Material( shape: const CircleBorder(), color: BbbColors.panel, - elevation: 0, child: InkWell( customBorder: const CircleBorder(), onTap: onResetBearing, diff --git a/mobile/lib/widgets/map/home_sheet_container.dart b/mobile/lib/widgets/map/home_sheet_container.dart index 227e299..da55249 100644 --- a/mobile/lib/widgets/map/home_sheet_container.dart +++ b/mobile/lib/widgets/map/home_sheet_container.dart @@ -1,19 +1,15 @@ +import 'package:beebeebike/widgets/brush_fab.dart'; +import 'package:beebeebike/widgets/home_sheet.dart'; +import 'package:beebeebike/widgets/map/compass_fab_inline.dart'; +import 'package:beebeebike/widgets/map/recenter_circle_fab.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import '../brush_fab.dart'; -import '../home_sheet.dart'; -import 'compass_fab_inline.dart'; -import 'recenter_circle_fab.dart'; - /// Home-mode bottom UI: the draggable [HomeSheet] plus a recenter/compass /// FAB column whose vertical position tracks the sheet's current snap size. class HomeSheetContainer extends ConsumerStatefulWidget { const HomeSheetContainer({ - super.key, - required this.onFlyToMyLocation, - required this.onResetBearing, - required this.onNavigateHome, + required this.onFlyToMyLocation, required this.onResetBearing, required this.onNavigateHome, super.key, }); final VoidCallback onFlyToMyLocation; diff --git a/mobile/lib/widgets/map/nav_top_bar.dart b/mobile/lib/widgets/map/nav_top_bar.dart index 4b4e98f..b8a7af8 100644 --- a/mobile/lib/widgets/map/nav_top_bar.dart +++ b/mobile/lib/widgets/map/nav_top_bar.dart @@ -1,28 +1,22 @@ +import 'package:beebeebike/l10n/generated/app_localizations.dart'; +import 'package:beebeebike/navigation/camera_controller.dart'; +import 'package:beebeebike/navigation/maneuver_icons.dart'; +import 'package:beebeebike/navigation/nav_constants.dart'; +import 'package:beebeebike/providers/navigation_camera_provider.dart'; +import 'package:beebeebike/providers/navigation_provider.dart'; +import 'package:beebeebike/widgets/map/compass_fab_inline.dart'; +import 'package:beebeebike/widgets/recenter_fab.dart'; +import 'package:beebeebike/widgets/rerouting_toast.dart'; +import 'package:beebeebike/widgets/tts_toggle_fab.dart'; +import 'package:beebeebike/widgets/turn_banner.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import '../../l10n/generated/app_localizations.dart'; -import '../../navigation/camera_controller.dart'; -import '../../navigation/maneuver_icons.dart'; -import '../../navigation/nav_constants.dart'; -import '../../providers/navigation_camera_provider.dart'; -import '../../providers/navigation_provider.dart'; -import '../recenter_fab.dart'; -import '../rerouting_toast.dart'; -import '../tts_toggle_fab.dart'; -import '../turn_banner.dart'; -import 'compass_fab_inline.dart'; - /// TurnBanner + RecenterFab, shown during navigation above the navigation /// sheet. class NavTopBar extends ConsumerWidget { const NavTopBar({ - super.key, - required this.ttsEnabled, - required this.rerouting, - required this.onToggleTts, - required this.onRecenter, - required this.onResetBearing, + required this.ttsEnabled, required this.rerouting, required this.onToggleTts, required this.onRecenter, required this.onResetBearing, super.key, }); final bool ttsEnabled; diff --git a/mobile/lib/widgets/map/navigation_sheet.dart b/mobile/lib/widgets/map/navigation_sheet.dart index 13e87a7..294db60 100644 --- a/mobile/lib/widgets/map/navigation_sheet.dart +++ b/mobile/lib/widgets/map/navigation_sheet.dart @@ -1,19 +1,17 @@ +import 'package:beebeebike/navigation/camera_controller.dart'; +import 'package:beebeebike/providers/navigation_camera_provider.dart'; +import 'package:beebeebike/providers/navigation_provider.dart'; +import 'package:beebeebike/theme/tokens.dart'; +import 'package:beebeebike/widgets/arrived_sheet.dart'; +import 'package:beebeebike/widgets/eta_sheet.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import '../../navigation/camera_controller.dart'; -import '../../providers/navigation_camera_provider.dart'; -import '../../providers/navigation_provider.dart'; -import '../../theme/tokens.dart'; -import '../arrived_sheet.dart'; -import '../eta_sheet.dart'; - /// Slides up over the route sheet during active navigation. Fixed height; /// not draggable. class NavigationSheet extends ConsumerWidget { const NavigationSheet({ - super.key, - required this.onClose, + required this.onClose, super.key, }); final VoidCallback onClose; diff --git a/mobile/lib/widgets/map/recenter_circle_fab.dart b/mobile/lib/widgets/map/recenter_circle_fab.dart index fbf4a76..6cbc1cb 100644 --- a/mobile/lib/widgets/map/recenter_circle_fab.dart +++ b/mobile/lib/widgets/map/recenter_circle_fab.dart @@ -1,9 +1,10 @@ -import 'package:flutter/material.dart'; +import 'dart:async'; -import '../../theme/tokens.dart'; +import 'package:beebeebike/theme/tokens.dart'; +import 'package:flutter/material.dart'; class RecenterCircleFab extends StatefulWidget { - const RecenterCircleFab({super.key, required this.onTap}); + const RecenterCircleFab({required this.onTap, super.key}); final VoidCallback onTap; @@ -17,7 +18,7 @@ class _RecenterCircleFabState extends State late final AnimationController _flash = AnimationController( vsync: this, duration: const Duration(milliseconds: 220), - value: 1.0, + value: 1, ); @override @@ -29,7 +30,7 @@ class _RecenterCircleFabState extends State void _handleTap() { setState(() => _pressed = false); widget.onTap(); - _flash.forward(from: 0); + unawaited(_flash.forward(from: 0)); } @override @@ -40,7 +41,6 @@ class _RecenterCircleFabState extends State curve: Curves.easeOut, child: Material( shape: const CircleBorder(), - elevation: 0, child: InkWell( customBorder: const CircleBorder(), onTapDown: (_) => setState(() => _pressed = true), diff --git a/mobile/lib/widgets/map/route_sheet.dart b/mobile/lib/widgets/map/route_sheet.dart index d1be76a..5ec26fa 100644 --- a/mobile/lib/widgets/map/route_sheet.dart +++ b/mobile/lib/widgets/map/route_sheet.dart @@ -1,26 +1,20 @@ +import 'package:beebeebike/l10n/generated/app_localizations.dart'; +import 'package:beebeebike/models/route_preview.dart'; +import 'package:beebeebike/models/route_state.dart'; +import 'package:beebeebike/providers/route_provider.dart'; +import 'package:beebeebike/theme/tokens.dart'; +import 'package:beebeebike/widgets/brush_fab.dart'; +import 'package:beebeebike/widgets/map/compass_fab_inline.dart'; +import 'package:beebeebike/widgets/map/recenter_circle_fab.dart'; +import 'package:beebeebike/widgets/route_summary.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import '../../l10n/generated/app_localizations.dart'; -import '../../models/route_preview.dart'; -import '../../models/route_state.dart'; -import '../../providers/route_provider.dart'; -import '../../theme/tokens.dart'; -import '../brush_fab.dart'; -import '../route_summary.dart'; -import 'compass_fab_inline.dart'; -import 'recenter_circle_fab.dart'; - /// Slides up over the home sheet when a route is active (loading, error, or /// preview ready). Fixed height; not draggable. class RouteSheet extends ConsumerWidget { const RouteSheet({ - super.key, - required this.routeState, - required this.preview, - required this.onFlyToMyLocation, - required this.onResetBearing, - required this.onStart, + required this.routeState, required this.preview, required this.onFlyToMyLocation, required this.onResetBearing, required this.onStart, super.key, }); final RouteState routeState; diff --git a/mobile/lib/widgets/onboarding_dots.dart b/mobile/lib/widgets/onboarding_dots.dart index 87a456e..4f8a395 100644 --- a/mobile/lib/widgets/onboarding_dots.dart +++ b/mobile/lib/widgets/onboarding_dots.dart @@ -1,9 +1,8 @@ +import 'package:beebeebike/theme/tokens.dart'; import 'package:flutter/material.dart'; -import '../theme/tokens.dart'; - class OnboardingDots extends StatelessWidget { - const OnboardingDots({super.key, required this.current, required this.total}); + const OnboardingDots({required this.current, required this.total, super.key}); final int current; final int total; diff --git a/mobile/lib/widgets/onboarding_page.dart b/mobile/lib/widgets/onboarding_page.dart index 790d1de..f29f3c8 100644 --- a/mobile/lib/widgets/onboarding_page.dart +++ b/mobile/lib/widgets/onboarding_page.dart @@ -1,14 +1,10 @@ +import 'package:beebeebike/theme/tokens.dart'; +import 'package:beebeebike/theme/typography.dart'; import 'package:flutter/material.dart'; -import '../theme/tokens.dart'; -import '../theme/typography.dart'; - class OnboardingPage extends StatelessWidget { const OnboardingPage({ - super.key, - required this.icon, - required this.eyebrow, - required this.headline, + required this.icon, required this.eyebrow, required this.headline, super.key, this.body, this.bullets, this.footer, diff --git a/mobile/lib/widgets/paint_roller_icon.dart b/mobile/lib/widgets/paint_roller_icon.dart index 4a54c9c..64e82d7 100644 --- a/mobile/lib/widgets/paint_roller_icon.dart +++ b/mobile/lib/widgets/paint_roller_icon.dart @@ -1,7 +1,6 @@ +import 'package:beebeebike/theme/tokens.dart'; import 'package:flutter/material.dart'; -import '../theme/tokens.dart'; - /// Custom paint-roller glyph — yellow roller head with ink frame + handle. /// 24×24 canvas per style-guide Paint FAB spec. class PaintRollerIcon extends StatelessWidget { @@ -52,15 +51,16 @@ class _PaintRollerPainter extends CustomPainter { Rect.fromLTWH(3 * scale, 4 * scale, 14 * scale, 5 * scale), Radius.circular(1.2 * scale), ); - canvas.drawRRect(headRect, Paint()..color = yellow); - canvas.drawRRect( - headRect, - Paint() - ..color = ink - ..style = PaintingStyle.stroke - ..strokeWidth = strokeW - ..strokeJoin = StrokeJoin.round, - ); + canvas + ..drawRRect(headRect, Paint()..color = yellow) + ..drawRRect( + headRect, + Paint() + ..color = ink + ..style = PaintingStyle.stroke + ..strokeWidth = strokeW + ..strokeJoin = StrokeJoin.round, + ); // Frame — short bracket down from the head center to the handle bend. final framePaint = Paint() diff --git a/mobile/lib/widgets/paint_sheet.dart b/mobile/lib/widgets/paint_sheet.dart index de80a78..fb60cf5 100644 --- a/mobile/lib/widgets/paint_sheet.dart +++ b/mobile/lib/widgets/paint_sheet.dart @@ -1,10 +1,9 @@ +import 'package:beebeebike/l10n/generated/app_localizations.dart'; +import 'package:beebeebike/providers/brush_provider.dart'; +import 'package:beebeebike/theme/tokens.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import '../l10n/generated/app_localizations.dart'; -import '../providers/brush_provider.dart'; -import '../theme/tokens.dart'; - class PaintSheet extends ConsumerWidget { const PaintSheet({super.key}); @@ -92,7 +91,7 @@ class _ColorChip extends StatelessWidget { ), transformAlignment: Alignment.center, transform: selected - ? Matrix4.diagonal3Values(1.15, 1.15, 1.0) + ? Matrix4.diagonal3Values(1.15, 1.15, 1) : Matrix4.identity(), child: isEraser ? Icon(Icons.cleaning_services_outlined, size: 20, color: color) diff --git a/mobile/lib/widgets/recenter_fab.dart b/mobile/lib/widgets/recenter_fab.dart index aa6a647..a0feccc 100644 --- a/mobile/lib/widgets/recenter_fab.dart +++ b/mobile/lib/widgets/recenter_fab.dart @@ -1,11 +1,10 @@ +import 'package:beebeebike/theme/tokens.dart'; import 'package:flutter/material.dart'; -import '../theme/tokens.dart'; - /// Aqua-brand recenter FAB — 52×52 circle, white crosshair icon. Used when /// the navigation camera has been panned off the user. class RecenterFab extends StatelessWidget { - const RecenterFab({super.key, required this.onTap}); + const RecenterFab({required this.onTap, super.key}); final VoidCallback onTap; diff --git a/mobile/lib/widgets/register_form.dart b/mobile/lib/widgets/register_form.dart index 8c766ae..bf3a531 100644 --- a/mobile/lib/widgets/register_form.dart +++ b/mobile/lib/widgets/register_form.dart @@ -1,11 +1,10 @@ +import 'package:beebeebike/l10n/generated/app_localizations.dart'; +import 'package:beebeebike/providers/auth_provider.dart'; import 'package:dio/dio.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import '../l10n/generated/app_localizations.dart'; -import '../providers/auth_provider.dart'; - enum _RegisterErrorKind { emailTaken, generic } class RegisterForm extends ConsumerStatefulWidget { diff --git a/mobile/lib/widgets/rerouting_toast.dart b/mobile/lib/widgets/rerouting_toast.dart index a413459..6364ad7 100644 --- a/mobile/lib/widgets/rerouting_toast.dart +++ b/mobile/lib/widgets/rerouting_toast.dart @@ -1,9 +1,8 @@ +import 'package:beebeebike/l10n/generated/app_localizations.dart'; +import 'package:beebeebike/theme/tokens.dart'; +import 'package:beebeebike/theme/typography.dart'; import 'package:flutter/material.dart'; -import '../l10n/generated/app_localizations.dart'; -import '../theme/tokens.dart'; -import '../theme/typography.dart'; - class ReroutingToast extends StatelessWidget { const ReroutingToast({super.key}); diff --git a/mobile/lib/widgets/route_card.dart b/mobile/lib/widgets/route_card.dart index f544e57..7a20172 100644 --- a/mobile/lib/widgets/route_card.dart +++ b/mobile/lib/widgets/route_card.dart @@ -1,16 +1,17 @@ +import 'dart:async'; + +import 'package:beebeebike/l10n/generated/app_localizations.dart'; +import 'package:beebeebike/models/location.dart'; +import 'package:beebeebike/providers/route_provider.dart'; +import 'package:beebeebike/providers/search_history_provider.dart'; +import 'package:beebeebike/screens/search_screen.dart'; +import 'package:beebeebike/screens/settings_screen.dart'; +import 'package:beebeebike/theme/tokens.dart'; +import 'package:beebeebike/theme/typography.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:geolocator/geolocator.dart'; -import '../l10n/generated/app_localizations.dart'; -import '../models/location.dart'; -import '../providers/route_provider.dart'; -import '../providers/search_history_provider.dart'; -import '../screens/search_screen.dart'; -import '../screens/settings_screen.dart'; -import '../theme/tokens.dart'; -import '../theme/typography.dart'; - class RouteCard extends ConsumerWidget { const RouteCard({super.key}); @@ -25,7 +26,7 @@ class RouteCard extends ConsumerWidget { lng: pos.longitude, lat: pos.latitude, ); - } catch (_) { + } on Object catch (_) { return Location( id: 'gps', name: label, @@ -39,36 +40,38 @@ class RouteCard extends ConsumerWidget { Future _openOriginSearch(BuildContext context, WidgetRef ref) async { final gpsLabel = AppLocalizations.of(context)!.locationCurrent; final result = await Navigator.of(context).push( - MaterialPageRoute(builder: (_) => const SearchScreen()), + MaterialPageRoute(builder: (_) => const SearchScreen()), ); if (result == null || !context.mounted) return; final origin = result.id == 'gps' ? await _resolveGps(gpsLabel) : result; if (!context.mounted) return; if (result.id != 'gps') { - ref.read(searchHistoryProvider.notifier).remember(result); + unawaited(ref.read(searchHistoryProvider.notifier).remember(result)); } - ref.read(routeControllerProvider.notifier).setOrigin(origin); + unawaited(ref.read(routeControllerProvider.notifier).setOrigin(origin)); } Future _openDestinationSearch( BuildContext context, WidgetRef ref) async { final gpsLabel = AppLocalizations.of(context)!.locationCurrent; final result = await Navigator.of(context).push( - MaterialPageRoute(builder: (_) => const SearchScreen()), + MaterialPageRoute(builder: (_) => const SearchScreen()), ); if (result == null || !context.mounted) return; final destination = result.id == 'gps' ? await _resolveGps(gpsLabel) : result; if (!context.mounted) return; if (result.id != 'gps') { - ref.read(searchHistoryProvider.notifier).remember(result); + unawaited(ref.read(searchHistoryProvider.notifier).remember(result)); } if (ref.read(routeControllerProvider).origin == null) { final gpsOrigin = await _resolveGps(gpsLabel); if (!context.mounted) return; - ref.read(routeControllerProvider.notifier).setOrigin(gpsOrigin); + unawaited(ref.read(routeControllerProvider.notifier).setOrigin(gpsOrigin)); } - ref.read(routeControllerProvider.notifier).setDestination(destination); + unawaited( + ref.read(routeControllerProvider.notifier).setDestination(destination), + ); } Future _swap(BuildContext context, WidgetRef ref) async { @@ -86,8 +89,10 @@ class RouteCard extends ConsumerWidget { newDestination = origin; } if (!context.mounted) return; - ref.read(routeControllerProvider.notifier).setOrigin(newOrigin); - ref.read(routeControllerProvider.notifier).setDestination(newDestination); + unawaited(ref.read(routeControllerProvider.notifier).setOrigin(newOrigin)); + unawaited( + ref.read(routeControllerProvider.notifier).setDestination(newDestination), + ); } @override @@ -127,8 +132,8 @@ class RouteCard extends ConsumerWidget { valueWeight: FontWeight.w600, trailing: _GhostIconButton( icon: Icons.person_outline, - onTap: () => Navigator.of(context).push( - MaterialPageRoute(builder: (_) => const SettingsScreen()), + onTap: () => Navigator.of(context).push( + MaterialPageRoute(builder: (_) => const SettingsScreen()), ), ), onTap: () => _openOriginSearch(context, ref), diff --git a/mobile/lib/widgets/route_summary.dart b/mobile/lib/widgets/route_summary.dart index ca96ef2..dec2f03 100644 --- a/mobile/lib/widgets/route_summary.dart +++ b/mobile/lib/widgets/route_summary.dart @@ -1,17 +1,13 @@ +import 'package:beebeebike/l10n/generated/app_localizations.dart'; +import 'package:beebeebike/theme/tokens.dart'; +import 'package:beebeebike/theme/typography.dart'; import 'package:flutter/material.dart'; -import '../l10n/generated/app_localizations.dart'; -import '../theme/tokens.dart'; -import '../theme/typography.dart'; - /// Route Sheet (variant B) body — mono data strip + Start ride / heart row. /// The heart is visually present but disabled in this build. class RouteSummary extends StatelessWidget { const RouteSummary({ - super.key, - required this.durationMinutes, - required this.distanceKm, - required this.onStart, + required this.durationMinutes, required this.distanceKm, required this.onStart, super.key, this.onClose, }); @@ -167,7 +163,7 @@ class _SaveButton extends StatelessWidget { decoration: BoxDecoration( color: BbbColors.panel, borderRadius: BorderRadius.circular(BbbRadius.ctrl), - border: Border.all(color: BbbColors.divider, width: 1), + border: Border.all(color: BbbColors.divider), ), child: const Icon( Icons.favorite_border, diff --git a/mobile/lib/widgets/saved_item.dart b/mobile/lib/widgets/saved_item.dart index ba97def..fdeb2d3 100644 --- a/mobile/lib/widgets/saved_item.dart +++ b/mobile/lib/widgets/saved_item.dart @@ -1,19 +1,12 @@ +import 'package:beebeebike/theme/tokens.dart'; +import 'package:beebeebike/theme/typography.dart'; import 'package:flutter/material.dart'; -import '../theme/tokens.dart'; -import '../theme/typography.dart'; - /// Row in the Home Sheet SAVED list. Left icon badge, title + mono sub, /// right-aligned mono time. Bottom hairline divider unless [isLast]. class SavedItem extends StatelessWidget { const SavedItem({ - super.key, - required this.icon, - required this.iconBg, - required this.iconColor, - required this.title, - required this.subtitle, - required this.time, + required this.icon, required this.iconBg, required this.iconColor, required this.title, required this.subtitle, required this.time, super.key, this.onTap, this.isLast = false, }); @@ -36,7 +29,7 @@ class SavedItem extends StatelessWidget { border: isLast ? null : const Border( - bottom: BorderSide(color: BbbColors.divider, width: 1), + bottom: BorderSide(color: BbbColors.divider), ), ), padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 12), diff --git a/mobile/lib/widgets/search_bar.dart b/mobile/lib/widgets/search_bar.dart index 3cd9aac..1c30a5c 100644 --- a/mobile/lib/widgets/search_bar.dart +++ b/mobile/lib/widgets/search_bar.dart @@ -1,12 +1,9 @@ +import 'package:beebeebike/l10n/generated/app_localizations.dart'; import 'package:flutter/material.dart'; -import '../l10n/generated/app_localizations.dart'; - class BeeBeeBikeSearchBar extends StatelessWidget { const BeeBeeBikeSearchBar({ - super.key, - required this.onTap, - required this.onAvatarTap, + required this.onTap, required this.onAvatarTap, super.key, }); final VoidCallback onTap; diff --git a/mobile/lib/widgets/tts_toggle_fab.dart b/mobile/lib/widgets/tts_toggle_fab.dart index d5e298b..34d6645 100644 --- a/mobile/lib/widgets/tts_toggle_fab.dart +++ b/mobile/lib/widgets/tts_toggle_fab.dart @@ -1,16 +1,13 @@ +import 'package:beebeebike/l10n/generated/app_localizations.dart'; +import 'package:beebeebike/theme/tokens.dart'; import 'package:flutter/material.dart'; -import '../l10n/generated/app_localizations.dart'; -import '../theme/tokens.dart'; - /// 52×52 circular FAB for toggling turn-by-turn voice during navigation. /// Active (voice on): dark ground, bright icon. Inactive: white ground, /// muted icon — matches the recenter-idle treatment. class TtsToggleFab extends StatelessWidget { const TtsToggleFab({ - super.key, - required this.enabled, - required this.onTap, + required this.enabled, required this.onTap, super.key, }); final bool enabled; @@ -25,7 +22,6 @@ class TtsToggleFab extends StatelessWidget { color: bg, surfaceTintColor: bg, shape: const CircleBorder(), - elevation: 0, child: InkWell( customBorder: const CircleBorder(), onTap: onTap, diff --git a/mobile/lib/widgets/turn_banner.dart b/mobile/lib/widgets/turn_banner.dart index dee8fbc..908d9a6 100644 --- a/mobile/lib/widgets/turn_banner.dart +++ b/mobile/lib/widgets/turn_banner.dart @@ -1,13 +1,10 @@ +import 'package:beebeebike/theme/tokens.dart'; +import 'package:beebeebike/theme/typography.dart'; import 'package:flutter/material.dart'; -import '../theme/tokens.dart'; -import '../theme/typography.dart'; - class TurnBanner extends StatelessWidget { const TurnBanner({ - super.key, - required this.primaryText, - required this.distanceText, + required this.primaryText, required this.distanceText, super.key, this.icon = Icons.straight, }); diff --git a/mobile/pubspec.lock b/mobile/pubspec.lock index a7c8d5b..b08fffd 100644 --- a/mobile/pubspec.lock +++ b/mobile/pubspec.lock @@ -1200,6 +1200,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.2.0" + very_good_analysis: + dependency: "direct dev" + description: + name: very_good_analysis + sha256: d1cb1d66a5aae2c702d68caca6c8347306d35e728fd94555fa21fa0448a972e0 + url: "https://pub.dev" + source: hosted + version: "10.2.0" vm_service: dependency: transitive description: diff --git a/mobile/pubspec.yaml b/mobile/pubspec.yaml index 60412b3..7fe06d0 100644 --- a/mobile/pubspec.yaml +++ b/mobile/pubspec.yaml @@ -8,47 +8,48 @@ environment: flutter: ^3.19.0 dependencies: + clipper2: ^0.0.3 + cookie_jar: ^4.0.8 + cupertino_icons: ^1.0.8 + dio: ^5.7.0 + dio_cookie_manager: ^3.1.1 + ferrostar_flutter: + path: ../packages/ferrostar_flutter flutter: sdk: flutter + flutter_compass: ^0.8.1 flutter_localizations: sdk: flutter - intl: any - cupertino_icons: ^1.0.8 flutter_riverpod: ^2.5.1 - dio: ^5.7.0 - dio_cookie_manager: ^3.1.1 - cookie_jar: ^4.0.8 - maplibre_gl: ^0.20.0 - geolocator: ^13.0.1 - flutter_compass: ^0.8.1 flutter_tts: ^4.0.2 - shared_preferences: ^2.3.2 - url_launcher: ^6.3.0 - webview_flutter: ^4.7.0 - path_provider: ^2.1.5 freezed_annotation: ^2.4.4 - turf: ^0.0.10 - clipper2: ^0.0.3 - json_annotation: ^4.9.0 + geolocator: ^13.0.1 google_fonts: ^6.2.1 haptic_feedback: ^0.6.4 + intl: any + json_annotation: ^4.9.0 + maplibre_gl: ^0.20.0 + path_provider: ^2.1.5 sentry_flutter: ^9.19.0 + shared_preferences: ^2.3.2 + turf: ^0.0.10 + url_launcher: ^6.3.0 wakelock_plus: ^1.2.8 - ferrostar_flutter: - path: ../packages/ferrostar_flutter + webview_flutter: ^4.7.0 dev_dependencies: + build_runner: ^2.4.13 + flutter_launcher_icons: ^0.14.4 + flutter_lints: ^5.0.0 flutter_test: sdk: flutter + freezed: ^2.5.7 + http_mock_adapter: ^0.6.1 integration_test: sdk: flutter - flutter_lints: ^5.0.0 - build_runner: ^2.4.13 - freezed: ^2.5.7 json_serializable: ^6.9.0 - http_mock_adapter: ^0.6.1 mocktail: ^1.0.4 - flutter_launcher_icons: ^0.14.4 + very_good_analysis: ^10.2.0 flutter_launcher_icons: ios: true diff --git a/mobile/test/api/paint_response_test.dart b/mobile/test/api/paint_response_test.dart index 036ca56..e93d76e 100644 --- a/mobile/test/api/paint_response_test.dart +++ b/mobile/test/api/paint_response_test.dart @@ -1,5 +1,5 @@ -import 'package:flutter_test/flutter_test.dart'; import 'package:beebeebike/models/paint_response.dart'; +import 'package:flutter_test/flutter_test.dart'; void main() { test('parses backend JSON with created_id and clipping counts', () { diff --git a/mobile/test/api/ratings_paint_api_test.dart b/mobile/test/api/ratings_paint_api_test.dart index ec426c3..a8b4e22 100644 --- a/mobile/test/api/ratings_paint_api_test.dart +++ b/mobile/test/api/ratings_paint_api_test.dart @@ -1,7 +1,7 @@ +import 'package:beebeebike/api/ratings_paint_api.dart'; import 'package:dio/dio.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:http_mock_adapter/http_mock_adapter.dart'; -import 'package:beebeebike/api/ratings_paint_api.dart'; void main() { late Dio dio; diff --git a/mobile/test/api/routing_api_test.dart b/mobile/test/api/routing_api_test.dart index 7a951cc..27158b8 100644 --- a/mobile/test/api/routing_api_test.dart +++ b/mobile/test/api/routing_api_test.dart @@ -1,7 +1,7 @@ +import 'package:beebeebike/api/routing_api.dart'; import 'package:dio/dio.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:http_mock_adapter/http_mock_adapter.dart'; -import 'package:beebeebike/api/routing_api.dart'; void main() { test('computeNavigationRoute returns raw JSON from /api/navigate', () async { @@ -34,9 +34,9 @@ void main() { const [13.405, 52.52], const [13.45, 52.51], ratingWeight: 0.5, - distanceInfluence: 70.0, + distanceInfluence: 70, ); - expect(json['routes'][0]['distance'], 1234.5); + expect(((json['routes'] as List).first as Map)['distance'], 1234.5); }); } diff --git a/mobile/test/app_smoke_test.dart b/mobile/test/app_smoke_test.dart index 052b10c..4ee6d82 100644 --- a/mobile/test/app_smoke_test.dart +++ b/mobile/test/app_smoke_test.dart @@ -1,13 +1,13 @@ -import 'package:flutter_test/flutter_test.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:shared_preferences/shared_preferences.dart'; -import 'package:beebeebike/app.dart'; import 'package:beebeebike/api/client.dart'; +import 'package:beebeebike/app.dart'; import 'package:beebeebike/config/app_config.dart'; import 'package:beebeebike/providers/onboarding_provider.dart'; import 'package:beebeebike/providers/search_history_provider.dart'; import 'package:beebeebike/services/map_style_loader.dart'; import 'package:dio/dio.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:shared_preferences/shared_preferences.dart'; void main() { testWidgets('boots to the map screen shell', (tester) async { @@ -49,7 +49,7 @@ void main() { dioProvider.overrideWithValue(dio), sharedPreferencesProvider.overrideWithValue(prefs), onboardingCompletedProvider - .overrideWith(() => _AlwaysDoneOnboarding()), + .overrideWith(_AlwaysDoneOnboarding.new), ], child: const BeeBeeBikeApp(), ), @@ -64,7 +64,7 @@ void main() { final prefs = await SharedPreferences.getInstance(); final dio = Dio(BaseOptions(baseUrl: 'http://localhost:3000')); - int authMeCallCount = 0; + var authMeCallCount = 0; dio.interceptors.add(InterceptorsWrapper( onRequest: (options, handler) { if (options.path == '/api/auth/me') { @@ -98,7 +98,7 @@ void main() { dioProvider.overrideWithValue(dio), sharedPreferencesProvider.overrideWithValue(prefs), onboardingCompletedProvider - .overrideWith(() => _AlwaysDoneOnboarding()), + .overrideWith(_AlwaysDoneOnboarding.new), ], child: const BeeBeeBikeApp(), ), @@ -115,8 +115,8 @@ void main() { final prefs = await SharedPreferences.getInstance(); final dio = Dio(BaseOptions(baseUrl: 'http://localhost:3000')); - int authMeCallCount = 0; - int anonCallCount = 0; + var authMeCallCount = 0; + var anonCallCount = 0; dio.interceptors.add(InterceptorsWrapper( onRequest: (options, handler) { if (options.path == '/api/auth/me') { diff --git a/mobile/test/helpers/test_helpers.dart b/mobile/test/helpers/test_helpers.dart index a54099b..51c32bf 100644 --- a/mobile/test/helpers/test_helpers.dart +++ b/mobile/test/helpers/test_helpers.dart @@ -182,7 +182,7 @@ Dio buildMockDio({ statusCode: 200, data: geocodeReturnsResults ? TestFixtures.geocodeResponse - : {'features': []}, + : {'features': []}, )); return; } @@ -225,9 +225,9 @@ Dio buildMockDio({ handler.resolve(Response( requestOptions: options, statusCode: 200, - data: const { + data: const { 'type': 'FeatureCollection', - 'features': [], + 'features': [], 'can_undo': false, 'can_redo': false, }, @@ -245,11 +245,11 @@ Dio buildMockDio({ handler.resolve(Response( requestOptions: options, statusCode: 200, - data: { + data: { 'id': 'home', - 'label': options.data?['label'] ?? 'Home', - 'lng': options.data?['lng'] ?? 13.4050, - 'lat': options.data?['lat'] ?? 52.5200, + 'label': (options.data as Map?)?['label'] ?? 'Home', + 'lng': (options.data as Map?)?['lng'] ?? 13.4050, + 'lat': (options.data as Map?)?['lat'] ?? 52.5200, }, )); } else if (options.method == 'DELETE') { diff --git a/mobile/test/navigation/camera_controller_test.dart b/mobile/test/navigation/camera_controller_test.dart index 4c0754e..e9eb950 100644 --- a/mobile/test/navigation/camera_controller_test.dart +++ b/mobile/test/navigation/camera_controller_test.dart @@ -1,5 +1,5 @@ -import 'package:flutter_test/flutter_test.dart'; import 'package:beebeebike/navigation/camera_controller.dart'; +import 'package:flutter_test/flutter_test.dart'; void main() { group('NavigationCameraController', () { @@ -10,73 +10,75 @@ void main() { }); test('onNavStart transitions awaitingFirstFix -> following', () { - final c = NavigationCameraController(); - c.onNavStart(); + final c = NavigationCameraController()..onNavStart(); expect(c.mode, CameraMode.following); }); test('onNavStart is a no-op if already following', () { - final c = NavigationCameraController()..onNavStart(); - c.onNavStart(); + final c = NavigationCameraController() + ..onNavStart() + ..onNavStart(); expect(c.mode, CameraMode.following); }); test('onTrackingDismissed transitions following -> free', () { - final c = NavigationCameraController()..onNavStart(); - c.onTrackingDismissed(); + final c = NavigationCameraController() + ..onNavStart() + ..onTrackingDismissed(); expect(c.mode, CameraMode.free); }); test('onTrackingDismissed transitions awaitingFirstFix -> free', () { // User can pan before the first GPS fix lands; we still want the // RecenterFab to surface so they can re-lock when ready. - final c = NavigationCameraController(); - c.onTrackingDismissed(); + final c = NavigationCameraController()..onTrackingDismissed(); expect(c.mode, CameraMode.free); }); test('onTrackingDismissed is a no-op in arrived', () { - final c = NavigationCameraController()..onArrived(); - c.onTrackingDismissed(); + final c = NavigationCameraController() + ..onArrived() + ..onTrackingDismissed(); expect(c.mode, CameraMode.arrived); }); test('onZoomChanged mutates followZoom iff mode == free', () { - final c = NavigationCameraController(); - c.onZoomChanged(14.0); + final c = NavigationCameraController()..onZoomChanged(14); expect(c.followZoom, 17.0); // awaitingFirstFix: ignored - c.onNavStart(); - c.onZoomChanged(15.5); + c + ..onNavStart() + ..onZoomChanged(15.5); expect(c.followZoom, 17.0); // following: ignored - c.onTrackingDismissed(); - c.onZoomChanged(13.2); + c + ..onTrackingDismissed() + ..onZoomChanged(13.2); expect(c.followZoom, 13.2); // free: captured }); test('onRecenterTapped transitions free -> following', () { final c = NavigationCameraController() ..onNavStart() - ..onTrackingDismissed(); - c.onRecenterTapped(); + ..onTrackingDismissed() + ..onRecenterTapped(); expect(c.mode, CameraMode.following); }); test('onRecenterTapped is a no-op in following', () { - final c = NavigationCameraController()..onNavStart(); - c.onRecenterTapped(); + final c = NavigationCameraController() + ..onNavStart() + ..onRecenterTapped(); expect(c.mode, CameraMode.following); }); test('onArrived transitions any state to arrived', () { for (final setup in [ - () => NavigationCameraController(), + NavigationCameraController.new, () => NavigationCameraController()..onNavStart(), () => NavigationCameraController() ..onNavStart() ..onTrackingDismissed(), ]) { - final c = setup(); - c.onArrived(); + final c = setup()..onArrived(); expect(c.mode, CameraMode.arrived); } }); @@ -84,24 +86,25 @@ void main() { test('notifies listeners on every successful transition', () { final c = NavigationCameraController(); var notifications = 0; - c.addListener(() => notifications++); - c.onNavStart(); - c.onTrackingDismissed(); - c.onZoomChanged(14.0); - c.onRecenterTapped(); - c.onArrived(); + c + ..addListener(() => notifications++) + ..onNavStart() + ..onTrackingDismissed() + ..onZoomChanged(14) + ..onRecenterTapped() + ..onArrived(); expect(notifications, 5); }); test('does not notify on no-op transitions', () { final c = NavigationCameraController(); var notifications = 0; - c.addListener(() => notifications++); - - // No-ops from awaitingFirstFix (onTrackingDismissed is a real - // transition in this state, so it is covered below). - c.onRecenterTapped(); - c.onZoomChanged(14.0); + c + ..addListener(() => notifications++) + // No-ops from awaitingFirstFix (onTrackingDismissed is a real + // transition in this state, so it is covered below). + ..onRecenterTapped() + ..onZoomChanged(14); expect(notifications, 0); // Real transition: -> following @@ -109,20 +112,23 @@ void main() { notifications = 0; // No-ops from following - c.onNavStart(); - c.onZoomChanged(14.0); + c + ..onNavStart() + ..onZoomChanged(14); expect(notifications, 0); // Real transitions: -> free, -> arrived - c.onTrackingDismissed(); - c.onArrived(); + c + ..onTrackingDismissed() + ..onArrived(); notifications = 0; // No-ops from arrived - c.onArrived(); - c.onTrackingDismissed(); - c.onRecenterTapped(); - c.onZoomChanged(14.0); + c + ..onArrived() + ..onTrackingDismissed() + ..onRecenterTapped() + ..onZoomChanged(14); expect(notifications, 0); }); }); diff --git a/mobile/test/navigation/location_converter_test.dart b/mobile/test/navigation/location_converter_test.dart index 2e97e61..f0c43ab 100644 --- a/mobile/test/navigation/location_converter_test.dart +++ b/mobile/test/navigation/location_converter_test.dart @@ -1,16 +1,15 @@ +import 'package:beebeebike/navigation/location_converter.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:geolocator/geolocator.dart'; import 'package:maplibre_gl/maplibre_gl.dart' as ml; -import 'package:beebeebike/navigation/location_converter.dart'; - void main() { test('maps Position fields to UserLocation', () { final pos = Position( latitude: 52.52, longitude: 13.405, accuracy: 4.5, - heading: 270.0, + heading: 270, speed: 3.2, timestamp: DateTime.fromMillisecondsSinceEpoch(1000), altitude: 0, @@ -34,7 +33,7 @@ void main() { latitude: 52.52, longitude: 13.405, accuracy: 5, - heading: 0.0, + heading: 0, speed: 0, timestamp: DateTime.fromMillisecondsSinceEpoch(0), altitude: 0, @@ -53,8 +52,8 @@ void main() { latitude: 52.52, longitude: 13.405, accuracy: 5, - heading: -1.0, - speed: -1.0, + heading: -1, + speed: -1, timestamp: DateTime.fromMillisecondsSinceEpoch(0), altitude: 0, altitudeAccuracy: 0, @@ -88,7 +87,7 @@ void main() { () { // Swift FFI traps on UInt16(-1.0). The Dart-side guard prevents the value // from reaching the bridge. - final r = maplibreToUserLocation(mlLoc(bearing: -1, speed: 4.0)); + final r = maplibreToUserLocation(mlLoc(bearing: -1, speed: 4)); expect(r.courseDeg, isNull); expect(r.speedMps, 4.0); }); @@ -102,7 +101,7 @@ void main() { }); test('maplibreToUserLocation passes valid bearing/speed through', () { - final r = maplibreToUserLocation(mlLoc(bearing: 90, speed: 3.0, accuracy: 5)); + final r = maplibreToUserLocation(mlLoc(bearing: 90, speed: 3, accuracy: 5)); expect(r.courseDeg, 90); expect(r.speedMps, 3.0); expect(r.horizontalAccuracyM, 5); diff --git a/mobile/test/navigation/navigation_service_test.dart b/mobile/test/navigation/navigation_service_test.dart index 9f4f6ae..ea487a0 100644 --- a/mobile/test/navigation/navigation_service_test.dart +++ b/mobile/test/navigation/navigation_service_test.dart @@ -1,11 +1,10 @@ import 'dart:async'; +import 'package:beebeebike/navigation/navigation_service.dart'; import 'package:ferrostar_flutter/ferrostar_flutter.dart'; import 'package:ferrostar_flutter/src/ferrostar_flutter_platform.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:beebeebike/navigation/navigation_service.dart'; - class FakeFerrostarFlutterPlatform extends FerrostarFlutterPlatform { final _deviationCtrl = StreamController.broadcast(); final _stateCtrl = StreamController.broadcast(); @@ -70,7 +69,7 @@ void main() { locationStreamFactory: () => const Stream.empty(), speakInstruction: (_) async {}, ); - addTearDown(() => service.dispose()); + addTearDown(service.dispose); await service.start( origin: const WaypointInput(lat: 52.52, lng: 13.405), @@ -78,10 +77,10 @@ void main() { ); fakePlatform.emitDeviation( - RouteDeviation( + const RouteDeviation( deviationM: 87, durationOffRouteMs: 12000, - userLocation: const UserLocation( + userLocation: UserLocation( lat: 52.521, lng: 13.406, horizontalAccuracyM: 5, @@ -113,7 +112,7 @@ void main() { spoken.add(text); }, ); - addTearDown(() => service.dispose()); + addTearDown(service.dispose); await service.start( origin: const WaypointInput(lat: 52.52, lng: 13.405), @@ -134,14 +133,16 @@ void main() { ); // Same uuid emitted many times (one per GPS tick in real usage). - fakePlatform.emitSpoken(first); - fakePlatform.emitSpoken(first); - fakePlatform.emitSpoken(first); + fakePlatform + ..emitSpoken(first) + ..emitSpoken(first) + ..emitSpoken(first); await pumpEventQueue(); // Next instruction with a different uuid. - fakePlatform.emitSpoken(second); - fakePlatform.emitSpoken(second); + fakePlatform + ..emitSpoken(second) + ..emitSpoken(second); await pumpEventQueue(); expect(spoken, [first.text, second.text]); @@ -161,7 +162,7 @@ void main() { locationStreamFactory: () => const Stream.empty(), speakInstruction: (_) async {}, ); - addTearDown(() => service.dispose()); + addTearDown(service.dispose); final received = []; service.stateStream.listen(received.add); @@ -192,7 +193,7 @@ void main() { }, locationStreamFactory: () => const Stream.empty(), speakInstruction: (_) async {}, - setWakelock: (enabled) async { + setWakelock: ({required enabled}) async { toggles.add(enabled); }, ); diff --git a/mobile/test/providers/auth_provider_test.dart b/mobile/test/providers/auth_provider_test.dart index 1a68ef0..6c8d5c1 100644 --- a/mobile/test/providers/auth_provider_test.dart +++ b/mobile/test/providers/auth_provider_test.dart @@ -1,9 +1,9 @@ +import 'package:beebeebike/api/client.dart'; +import 'package:beebeebike/providers/auth_provider.dart'; import 'package:dio/dio.dart'; -import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_test/flutter_test.dart'; import 'package:http_mock_adapter/http_mock_adapter.dart'; -import 'package:beebeebike/api/client.dart'; -import 'package:beebeebike/providers/auth_provider.dart'; void main() { test('bootstraps anonymous session when /api/auth/me returns 401', () async { @@ -11,16 +11,17 @@ void main() { final adapter = DioAdapter(dio: dio); dio.httpClientAdapter = adapter; - adapter.onGet('/api/auth/me', (server) => server.reply(401, {'error': 'unauthorized'})); - adapter.onPost( - '/api/auth/anonymous', - (server) => server.reply(200, { - 'id': 'user-1', - 'account_type': 'anonymous', - 'display_name': '', - 'email': null, - }), - ); + adapter + ..onGet('/api/auth/me', (server) => server.reply(401, {'error': 'unauthorized'})) + ..onPost( + '/api/auth/anonymous', + (server) => server.reply(200, { + 'id': 'user-1', + 'account_type': 'anonymous', + 'display_name': '', + 'email': null, + }), + ); final container = ProviderContainer(overrides: [ dioProvider.overrideWithValue(dio), diff --git a/mobile/test/providers/brush_controller_test.dart b/mobile/test/providers/brush_controller_test.dart index 628e937..27a4f3e 100644 --- a/mobile/test/providers/brush_controller_test.dart +++ b/mobile/test/providers/brush_controller_test.dart @@ -1,12 +1,11 @@ -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:maplibre_gl/maplibre_gl.dart'; -import 'package:mocktail/mocktail.dart'; - import 'package:beebeebike/api/ratings_paint_api.dart'; import 'package:beebeebike/models/paint_response.dart'; import 'package:beebeebike/providers/brush_provider.dart'; import 'package:beebeebike/services/brush_overlay.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:maplibre_gl/maplibre_gl.dart'; +import 'package:mocktail/mocktail.dart'; class _MockApi extends Mock implements RatingsPaintApi {} @@ -32,8 +31,6 @@ class _FakeSurface implements BrushOverlaySurface { PaintResponse _ok({bool undo = true, bool redo = false}) => PaintResponse( createdId: 1, - clippedCount: 0, - deletedCount: 0, canUndo: undo, canRedo: redo, ); @@ -53,8 +50,7 @@ void main() { container = ProviderContainer(overrides: [ ratingsPaintApiProvider.overrideWithValue(api), ]); - final notifier = container.read(brushControllerProvider.notifier); - notifier.attach(surface: surface); + container.read(brushControllerProvider.notifier).attach(surface); }); tearDown(() => container.dispose()); @@ -134,7 +130,6 @@ void main() { final captured = verify(() => api.paint( geometry: captureAny(named: 'geometry'), value: 3, - targetId: null, )).captured.single as Map; expect(captured['type'], 'Polygon'); }); diff --git a/mobile/test/providers/navigation_provider_test.dart b/mobile/test/providers/navigation_provider_test.dart index 09f4ca0..4d4180a 100644 --- a/mobile/test/providers/navigation_provider_test.dart +++ b/mobile/test/providers/navigation_provider_test.dart @@ -54,9 +54,7 @@ void main() { final prefs = await SharedPreferences.getInstance(); final tts = MockFlutterTts(); when(() => tts.setLanguage(any())).thenAnswer((_) async => 1); - final container = _container(prefs: prefs, tts: tts); - - container.read(flutterTtsProvider); + _container(prefs: prefs, tts: tts).read(flutterTtsProvider); verify(() => tts.setLanguage('de-DE')).called(1); }); @@ -66,9 +64,7 @@ void main() { final prefs = await SharedPreferences.getInstance(); final tts = MockFlutterTts(); when(() => tts.setLanguage(any())).thenAnswer((_) async => 1); - final container = _container(prefs: prefs, tts: tts); - - container.read(flutterTtsProvider); + _container(prefs: prefs, tts: tts).read(flutterTtsProvider); verify(() => tts.setLanguage('en-US')).called(1); }); diff --git a/mobile/test/providers/rating_overlay_controller_test.dart b/mobile/test/providers/rating_overlay_controller_test.dart index 49ce70e..d421ed4 100644 --- a/mobile/test/providers/rating_overlay_controller_test.dart +++ b/mobile/test/providers/rating_overlay_controller_test.dart @@ -45,9 +45,9 @@ class _FakeOverlay implements RatingOverlaySurface { class _FakeRatingsApi implements RatingsApi { final List<({String bbox, CancelToken? token})> calls = []; bool blockNext = false; - Map response = const { + Map response = const { 'type': 'FeatureCollection', - 'features': [], + 'features': [], }; @override @@ -60,14 +60,17 @@ class _FakeRatingsApi implements RatingsApi { // Resolve only when the CancelToken is cancelled — mirrors Dio's // real behavior, so the controller's `on DioException` catch runs. final completer = Completer>(); - cancelToken?.whenCancel.then((_) { - if (!completer.isCompleted) { - completer.completeError(DioException( - requestOptions: RequestOptions(path: '/api/ratings'), - type: DioExceptionType.cancel, - )); - } - }); + final whenCancel = cancelToken?.whenCancel; + if (whenCancel != null) { + unawaited(whenCancel.then((_) { + if (!completer.isCompleted) { + completer.completeError(DioException( + requestOptions: RequestOptions(path: '/api/ratings'), + type: DioExceptionType.cancel, + )); + } + })); + } return completer.future; } return response; @@ -149,7 +152,7 @@ class _FakeEventsClient implements RatingEventsClient { bool get serverDisabled => _serverDisabled; @override - noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation); + dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation); } _Setup _setup({ @@ -322,7 +325,7 @@ void main() { final container = ProviderContainer(overrides: [ appConfigProvider.overrideWithValue(_sseDisabledConfig), ratingsApiProvider.overrideWithValue(api), - authControllerProvider.overrideWith(() => _PendingAuthController()), + authControllerProvider.overrideWith(_PendingAuthController.new), ]); addTearDown(container.dispose); @@ -337,7 +340,6 @@ void main() { test('does not start events client when SSE disabled in config', () async { final s = _setup( - config: _sseDisabledConfig, installFakeEventsClient: true, ); addTearDown(s.container.dispose); @@ -474,7 +476,7 @@ void main() { final container = ProviderContainer(overrides: [ appConfigProvider.overrideWithValue(_sseEnabledConfig), ratingsApiProvider.overrideWithValue(api), - authControllerProvider.overrideWith(() => _PendingAuthController()), + authControllerProvider.overrideWith(_PendingAuthController.new), ratingEventsClientFactoryProvider.overrideWithValue( (onInvalidate, {onServerDisabled}) { capturedClient = _FakeEventsClient(onInvalidate, onServerDisabled); diff --git a/mobile/test/providers/route_provider_test.dart b/mobile/test/providers/route_provider_test.dart index 99faa53..20040e3 100644 --- a/mobile/test/providers/route_provider_test.dart +++ b/mobile/test/providers/route_provider_test.dart @@ -1,17 +1,17 @@ -import 'package:flutter_test/flutter_test.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:beebeebike/models/location.dart'; import 'package:beebeebike/models/route_preview.dart'; import 'package:beebeebike/models/user.dart'; import 'package:beebeebike/providers/auth_provider.dart'; import 'package:beebeebike/providers/route_provider.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_test/flutter_test.dart'; void main() { test('setDestination computes a preview when origin already exists', () async { final container = ProviderContainer(overrides: [ routePreviewLoaderProvider.overrideWithValue( - ({required origin, required destination}) async => RoutePreview( - geometry: const {'type': 'LineString', 'coordinates': []}, + ({required origin, required destination}) async => const RoutePreview( + geometry: {'type': 'LineString', 'coordinates': []}, distance: 3200, time: 720, ), @@ -36,13 +36,13 @@ void main() { ({required origin, required destination}) async { callCount++; return RoutePreview( - geometry: const {'type': 'LineString', 'coordinates': []}, + geometry: const {'type': 'LineString', 'coordinates': []}, distance: 1000.0 * callCount, - time: 300000.0, + time: 300000, ); }, ), - authControllerProvider.overrideWith(() => _FakeAuthController()), + authControllerProvider.overrideWith(_FakeAuthController.new), ]); addTearDown(container.dispose); @@ -59,7 +59,7 @@ void main() { (container.read(authControllerProvider.notifier) as _FakeAuthController) .simulateLogin(); // Allow Riverpod listeners to fire. - await Future.delayed(Duration.zero); + await Future.delayed(Duration.zero); expect(callCount, 2); expect(container.read(routeControllerProvider).preview?.distance, 2000.0); diff --git a/mobile/test/screens/map_screen_navigation_test.dart b/mobile/test/screens/map_screen_navigation_test.dart index 17f7499..012f8fb 100644 --- a/mobile/test/screens/map_screen_navigation_test.dart +++ b/mobile/test/screens/map_screen_navigation_test.dart @@ -61,7 +61,7 @@ Future<_NavHarness> _pumpNavActive(WidgetTester tester) async { final cam = NavigationCameraController(); final fakeService = NavigationService( - createController: (_, __) => throw UnimplementedError(), + createController: (_, _) => throw UnimplementedError(), loadNavigationRoute: ({required origin, required destination}) => throw UnimplementedError(), locationStreamFactory: () => const Stream.empty(), @@ -88,11 +88,11 @@ Future<_NavHarness> _pumpNavActive(WidgetTester tester) async { navigationServiceProvider.overrideWithValue(fakeService), navigationCameraControllerProvider.overrideWith((ref) => cam), ], - child: MaterialApp( - locale: const Locale('en'), + child: const MaterialApp( + locale: Locale('en'), localizationsDelegates: AppLocalizations.localizationsDelegates, supportedLocales: AppLocalizations.supportedLocales, - home: const MapScreen(), + home: MapScreen(), ), ), ); diff --git a/mobile/test/screens/map_screen_test.dart b/mobile/test/screens/map_screen_test.dart index 26ae694..fd9fdc0 100644 --- a/mobile/test/screens/map_screen_test.dart +++ b/mobile/test/screens/map_screen_test.dart @@ -43,11 +43,11 @@ void main() { ...testProviderOverrides(prefs: prefs), routeControllerProvider.overrideWith(_LoadingRouteController.new), ], - child: MaterialApp( - locale: const Locale('en'), + child: const MaterialApp( + locale: Locale('en'), localizationsDelegates: AppLocalizations.localizationsDelegates, supportedLocales: AppLocalizations.supportedLocales, - home: const MapScreen(), + home: MapScreen(), ), ), ); @@ -65,11 +65,11 @@ void main() { ...testProviderOverrides(prefs: prefs), routeControllerProvider.overrideWith(_ErrorRouteController.new), ], - child: MaterialApp( - locale: const Locale('en'), + child: const MaterialApp( + locale: Locale('en'), localizationsDelegates: AppLocalizations.localizationsDelegates, supportedLocales: AppLocalizations.supportedLocales, - home: const MapScreen(), + home: MapScreen(), ), ), ); @@ -87,11 +87,11 @@ void main() { ...testProviderOverrides(prefs: prefs), routeControllerProvider.overrideWith(_PreviewRouteController.new), ], - child: MaterialApp( - locale: const Locale('en'), + child: const MaterialApp( + locale: Locale('en'), localizationsDelegates: AppLocalizations.localizationsDelegates, supportedLocales: AppLocalizations.supportedLocales, - home: const MapScreen(), + home: MapScreen(), ), ), ); @@ -117,11 +117,11 @@ void main() { await tester.pumpWidget( UncontrolledProviderScope( container: container, - child: MaterialApp( - locale: const Locale('en'), + child: const MaterialApp( + locale: Locale('en'), localizationsDelegates: AppLocalizations.localizationsDelegates, supportedLocales: AppLocalizations.supportedLocales, - home: const MapScreen(), + home: MapScreen(), ), ), ); diff --git a/mobile/test/screens/onboarding_screen_test.dart b/mobile/test/screens/onboarding_screen_test.dart index 9498f87..48da799 100644 --- a/mobile/test/screens/onboarding_screen_test.dart +++ b/mobile/test/screens/onboarding_screen_test.dart @@ -97,7 +97,6 @@ void main() { }, ), prefs: prefs, - loginSucceeds: true, ), ); await tester.pumpAndSettle(); @@ -137,7 +136,6 @@ void main() { }, ), prefs: prefs, - loginSucceeds: true, ), ); await tester.pumpAndSettle(); @@ -174,7 +172,7 @@ void main() { ]); addTearDown(container.dispose); - final expected = kAlwaysShowOnboarding ? isFalse : isTrue; + const expected = kAlwaysShowOnboarding ? isFalse : isTrue; expect(container.read(onboardingCompletedProvider), expected); }); } diff --git a/mobile/test/screens/search_screen_test.dart b/mobile/test/screens/search_screen_test.dart index 607feac..50fceb6 100644 --- a/mobile/test/screens/search_screen_test.dart +++ b/mobile/test/screens/search_screen_test.dart @@ -151,7 +151,6 @@ void main() { buildTestWidget( const SearchScreen(), prefs: prefs, - geocodeReturnsResults: true, ), ); @@ -170,7 +169,6 @@ void main() { buildTestWidget( const SearchScreen(), prefs: prefs, - geocodeReturnsResults: true, ), ); @@ -192,7 +190,6 @@ void main() { buildTestWidget( const SearchScreen(), prefs: prefs, - geocodeReturnsResults: true, ), ); @@ -252,7 +249,6 @@ void main() { ), ), prefs: prefs, - geocodeReturnsResults: true, ), ); diff --git a/mobile/test/screens/settings_danger_zone_test.dart b/mobile/test/screens/settings_danger_zone_test.dart index a302440..f0af805 100644 --- a/mobile/test/screens/settings_danger_zone_test.dart +++ b/mobile/test/screens/settings_danger_zone_test.dart @@ -25,7 +25,6 @@ void main() { buildTestWidget( const SettingsScreen(), prefs: prefs, - authenticated: false, ), ); await tester.pumpAndSettle(); @@ -52,7 +51,7 @@ void main() { testWidgets('confirm dialog cancels do not call delete', (tester) async { final prefs = await SharedPreferences.getInstance(); await pumpTall(tester); - int deleteCalls = 0; + var deleteCalls = 0; final dio = _mockDioForDelete(onDelete: () => deleteCalls++); await tester.pumpWidget( @@ -89,7 +88,7 @@ void main() { (tester) async { final prefs = await SharedPreferences.getInstance(); await pumpTall(tester); - int deleteCalls = 0; + var deleteCalls = 0; final dio = _mockDioForDelete(onDelete: () => deleteCalls++); await tester.pumpWidget( diff --git a/mobile/test/screens/settings_login_test.dart b/mobile/test/screens/settings_login_test.dart index cd88bd2..586bc40 100644 --- a/mobile/test/screens/settings_login_test.dart +++ b/mobile/test/screens/settings_login_test.dart @@ -19,7 +19,6 @@ void main() { buildTestWidget( const SettingsScreen(), prefs: prefs, - authenticated: false, ), ); await tester.pumpAndSettle(); @@ -35,7 +34,6 @@ void main() { buildTestWidget( const SettingsScreen(), prefs: prefs, - authenticated: false, ), ); await tester.pumpAndSettle(); @@ -55,7 +53,6 @@ void main() { buildTestWidget( const SettingsScreen(), prefs: prefs, - authenticated: false, ), ); await tester.pumpAndSettle(); @@ -131,14 +128,13 @@ void main() { buildTestWidget( Builder( builder: (context) => ElevatedButton( - onPressed: () => Navigator.of(context).push( - MaterialPageRoute(builder: (_) => const LoginScreen()), + onPressed: () => Navigator.of(context).push( + MaterialPageRoute(builder: (_) => const LoginScreen()), ), child: const Text('Open Login'), ), ), prefs: prefs, - loginSucceeds: true, ), ); await tester.pumpAndSettle(); diff --git a/mobile/test/services/brush_geometry_test.dart b/mobile/test/services/brush_geometry_test.dart index 396fabe..d2f763a 100644 --- a/mobile/test/services/brush_geometry_test.dart +++ b/mobile/test/services/brush_geometry_test.dart @@ -1,7 +1,7 @@ import 'dart:math' as math; -import 'package:flutter_test/flutter_test.dart'; import 'package:beebeebike/services/brush_geometry.dart'; +import 'package:flutter_test/flutter_test.dart'; import 'package:maplibre_gl/maplibre_gl.dart'; void main() { @@ -124,7 +124,7 @@ void main() { /// Positive area = CCW (per GeoJSON right-hand rule for exterior rings). /// Negative area = CW. /// Parameters: coords is a list of [lng, lat] pairs. -double _signedArea(List coords) { +double _signedArea(List coords) { double s = 0; for (var i = 0; i < coords.length - 1; i++) { final a = coords[i] as List; diff --git a/mobile/test/services/brush_overlay_test.dart b/mobile/test/services/brush_overlay_test.dart index 643acf8..3392951 100644 --- a/mobile/test/services/brush_overlay_test.dart +++ b/mobile/test/services/brush_overlay_test.dart @@ -1,5 +1,5 @@ -import 'package:flutter_test/flutter_test.dart'; import 'package:beebeebike/services/brush_overlay.dart'; +import 'package:flutter_test/flutter_test.dart'; class _FakeSurface implements BrushOverlaySurface { @override @@ -40,7 +40,7 @@ void main() { test('BrushOverlaySurface contract is usable via a fake', () async { final fake = _FakeSurface(); await fake.setPreview( - const {'type': 'Polygon', 'coordinates': []}, + const {'type': 'Polygon', 'coordinates': []}, '#1abc9c', ); expect(fake.lastColor, '#1abc9c'); diff --git a/mobile/test/services/rating_events_client_test.dart b/mobile/test/services/rating_events_client_test.dart index fef2b59..0d978da 100644 --- a/mobile/test/services/rating_events_client_test.dart +++ b/mobile/test/services/rating_events_client_test.dart @@ -106,8 +106,7 @@ void main() { // Zero backoff — we shouldn't reach it here anyway. backoff: (_) => Duration.zero, sleep: (_) async {}, - ); - client.start(); + )..start(); await _pump(); expect(calls, 1, reason: 'force-invalidate on first connect'); @@ -130,13 +129,13 @@ void main() { opener: ({required cancelToken}) async => controller.stream, onInvalidate: () => calls++, sleep: (_) async {}, - ); - client.start(); + )..start(); await _pump(); final baseline = calls; // 1 (force-invalidate on connect) - controller.add(_bytes('event: keepalive\ndata: ok\n\n')); - controller.add(_bytes('data: plain message\n\n')); + controller + ..add(_bytes('event: keepalive\ndata: ok\n\n')) + ..add(_bytes('data: plain message\n\n')); await _pump(); expect(calls, baseline, reason: 'only invalidate events count'); @@ -158,8 +157,7 @@ void main() { onInvalidate: () => calls++, backoff: (_) => Duration.zero, sleep: (_) async {}, - ); - client.start(); + )..start(); await _pump(); expect(opens, 1); expect(calls, 1, reason: 'force-invalidate on first connect'); @@ -192,8 +190,7 @@ void main() { // Fast path to detect any accidental retry. backoff: (_) => Duration.zero, sleep: (_) async {}, - ); - client.start(); + )..start(); await _pump(); expect(client.serverDisabled, isTrue); expect(opens, 1, reason: '404 must not retry'); @@ -231,8 +228,7 @@ void main() { sleepCompleter.add(c); return c.future; }, - ); - client.start(); + )..start(); await _pump(); expect(opens, 1); expect(delays, [const Duration(seconds: 1)], @@ -260,8 +256,7 @@ void main() { onInvalidate: () {}, backoff: (_) => const Duration(hours: 1), sleep: (_) => sleepCompleter.future, - ); - client.start(); + )..start(); await _pump(); // Now in backoff sleep — stop() must unblock us. final stopFuture = client.stop(); diff --git a/mobile/test/widgets/home_sheet_test.dart b/mobile/test/widgets/home_sheet_test.dart index 52e7384..71ebd23 100644 --- a/mobile/test/widgets/home_sheet_test.dart +++ b/mobile/test/widgets/home_sheet_test.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'dart:convert'; import 'package:beebeebike/l10n/generated/app_localizations.dart'; @@ -111,7 +112,7 @@ void main() { // Pre-seed origin so the tap handler skips the Geolocator GPS branch, // which has no platform channel in the test env. - container.read(routeControllerProvider.notifier).setOrigin(fakeOrigin()); + unawaited(container.read(routeControllerProvider.notifier).setOrigin(fakeOrigin())); expect(container.read(routeControllerProvider).destination, isNull); diff --git a/mobile/test/widgets/paint_sheet_test.dart b/mobile/test/widgets/paint_sheet_test.dart index 5818faa..f5bcf7a 100644 --- a/mobile/test/widgets/paint_sheet_test.dart +++ b/mobile/test/widgets/paint_sheet_test.dart @@ -1,12 +1,11 @@ +import 'package:beebeebike/l10n/generated/app_localizations.dart'; +import 'package:beebeebike/providers/brush_provider.dart'; +import 'package:beebeebike/widgets/paint_sheet.dart'; import 'package:flutter/material.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:beebeebike/l10n/generated/app_localizations.dart'; -import 'package:beebeebike/providers/brush_provider.dart'; -import 'package:beebeebike/widgets/paint_sheet.dart'; - Widget _wrap(Widget child) { return MaterialApp( localizationsDelegates: const [ diff --git a/mobile/test/widgets/rerouting_toast_test.dart b/mobile/test/widgets/rerouting_toast_test.dart index eaf5b2a..7b1fcb3 100644 --- a/mobile/test/widgets/rerouting_toast_test.dart +++ b/mobile/test/widgets/rerouting_toast_test.dart @@ -5,11 +5,11 @@ import 'package:flutter_test/flutter_test.dart'; void main() { testWidgets('renders text and spinner', (tester) async { - await tester.pumpWidget(MaterialApp( - locale: const Locale('en'), + await tester.pumpWidget(const MaterialApp( + locale: Locale('en'), localizationsDelegates: AppLocalizations.localizationsDelegates, supportedLocales: AppLocalizations.supportedLocales, - home: const Scaffold(body: ReroutingToast()), + home: Scaffold(body: ReroutingToast()), )); expect(find.text('Rerouting…'), findsOneWidget); expect(find.byType(CircularProgressIndicator), findsOneWidget); diff --git a/mobile/test/widgets/route_card_test.dart b/mobile/test/widgets/route_card_test.dart index 53f9698..479027a 100644 --- a/mobile/test/widgets/route_card_test.dart +++ b/mobile/test/widgets/route_card_test.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:beebeebike/l10n/generated/app_localizations.dart'; import 'package:beebeebike/models/location.dart'; import 'package:beebeebike/providers/route_provider.dart'; @@ -50,7 +52,7 @@ void main() { ), ); - container.read(routeControllerProvider.notifier).setOrigin( + unawaited(container.read(routeControllerProvider.notifier).setOrigin( const Location( id: 'N:1', name: 'Brandenburger Tor', @@ -58,7 +60,7 @@ void main() { lng: 13.3777, lat: 52.5163, ), - ); + )); await tester.pump(); expect(find.text('Brandenburger Tor'), findsOneWidget); @@ -79,7 +81,7 @@ void main() { ), ); - container.read(routeControllerProvider.notifier).setOrigin( + unawaited(container.read(routeControllerProvider.notifier).setOrigin( const Location( id: 'gps', name: 'Current location', @@ -87,7 +89,7 @@ void main() { lng: 13.4533, lat: 52.5065, ), - ); + )); await tester.pump(); expect(find.text('Current location'), findsOneWidget); @@ -108,7 +110,7 @@ void main() { ), ); - container.read(routeControllerProvider.notifier).setDestination( + unawaited(container.read(routeControllerProvider.notifier).setDestination( const Location( id: 'N:42', name: 'Alexanderplatz', @@ -116,7 +118,7 @@ void main() { lng: 13.4050, lat: 52.5200, ), - ); + )); await tester.pump(); expect(find.text('Alexanderplatz'), findsOneWidget); @@ -160,9 +162,9 @@ void main() { ), ); - container.read(routeControllerProvider.notifier).setDestination( + unawaited(container.read(routeControllerProvider.notifier).setDestination( fakeDest(), - ); + )); await tester.pump(); final swapButton = tester.widget( @@ -183,7 +185,7 @@ void main() { addTearDown(container.dispose); // No origin set — destination alone must not compute a route - container.read(routeControllerProvider.notifier).setDestination( + unawaited(container.read(routeControllerProvider.notifier).setDestination( const Location( id: 'N:42', name: 'Alexanderplatz', @@ -191,7 +193,7 @@ void main() { lng: 13.4050, lat: 52.5200, ), - ); + )); final state = container.read(routeControllerProvider); expect(state.origin, isNull); @@ -203,7 +205,7 @@ void main() { SharedPreferences.setMockInitialValues({}); final prefs = await SharedPreferences.getInstance(); final container = ProviderContainer( - overrides: testProviderOverrides(prefs: prefs, routeSucceeds: true), + overrides: testProviderOverrides(prefs: prefs), ); addTearDown(container.dispose); diff --git a/mobile/test/widgets/route_summary_test.dart b/mobile/test/widgets/route_summary_test.dart index fd6bba4..2fd622c 100644 --- a/mobile/test/widgets/route_summary_test.dart +++ b/mobile/test/widgets/route_summary_test.dart @@ -110,7 +110,7 @@ void main() { home: Scaffold( body: RouteSummary( durationMinutes: 20, - distanceKm: 5.0, + distanceKm: 5, onStart: () {}, ), ), diff --git a/packages/ferrostar_flutter/analysis_options.yaml b/packages/ferrostar_flutter/analysis_options.yaml index 3882ff4..c2b4f2a 100644 --- a/packages/ferrostar_flutter/analysis_options.yaml +++ b/packages/ferrostar_flutter/analysis_options.yaml @@ -1,21 +1,8 @@ -include: package:flutter_lints/flutter.yaml +include: package:very_good_analysis/analysis_options.yaml analyzer: - language: - strict-casts: true - strict-raw-types: true errors: - missing_required_param: error - missing_return: error invalid_annotation_target: ignore exclude: - "**/*.g.dart" - "**/*.freezed.dart" - -linter: - rules: - - always_declare_return_types - - avoid_dynamic_calls - - avoid_print - - prefer_const_constructors - - prefer_final_locals diff --git a/packages/ferrostar_flutter/example/analysis_options.yaml b/packages/ferrostar_flutter/example/analysis_options.yaml index 0d29021..2a543b2 100644 --- a/packages/ferrostar_flutter/example/analysis_options.yaml +++ b/packages/ferrostar_flutter/example/analysis_options.yaml @@ -1,28 +1,8 @@ -# This file configures the analyzer, which statically analyzes Dart code to -# check for errors, warnings, and lints. -# -# The issues identified by the analyzer are surfaced in the UI of Dart-enabled -# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be -# invoked from the command line by running `flutter analyze`. - -# The following line activates a set of recommended lints for Flutter apps, -# packages, and plugins designed to encourage good coding practices. -include: package:flutter_lints/flutter.yaml +include: package:very_good_analysis/analysis_options.yaml linter: - # The lint rules applied to this project can be customized in the - # section below to disable rules from the `package:flutter_lints/flutter.yaml` - # included above or to enable additional rules. A list of all available lints - # and their documentation is published at https://dart.dev/lints. - # - # Instead of disabling a lint rule for the entire project in the - # section below, it can also be suppressed for a single line of code - # or a specific dart file by using the `// ignore: name_of_lint` and - # `// ignore_for_file: name_of_lint` syntax on the line or in the file - # producing the lint. rules: - # avoid_print: false # Uncomment to disable the `avoid_print` rule - # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule - -# Additional information about this file can be found at -# https://dart.dev/guides/language/analysis-options + # Demo app, not a published library. + public_member_api_docs: false + # `dart format` is authoritative for line length. + lines_longer_than_80_chars: false diff --git a/packages/ferrostar_flutter/example/integration_test/plugin_integration_test.dart b/packages/ferrostar_flutter/example/integration_test/plugin_integration_test.dart index fdfcea1..77000f1 100644 --- a/packages/ferrostar_flutter/example/integration_test/plugin_integration_test.dart +++ b/packages/ferrostar_flutter/example/integration_test/plugin_integration_test.dart @@ -1,8 +1,9 @@ import 'dart:convert'; + +import 'package:ferrostar_flutter/ferrostar_flutter.dart'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; -import 'package:ferrostar_flutter/ferrostar_flutter.dart'; void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); @@ -26,8 +27,8 @@ void main() { await ctrl.updateLocation(UserLocation( lat: 59.4429, lng: 24.7653, - horizontalAccuracyM: 5.0, - courseDeg: 315.0, + horizontalAccuracyM: 5, + courseDeg: 315, timestampMs: DateTime.now().millisecondsSinceEpoch, )); diff --git a/packages/ferrostar_flutter/example/lib/main.dart b/packages/ferrostar_flutter/example/lib/main.dart index 26a0706..0e90da3 100644 --- a/packages/ferrostar_flutter/example/lib/main.dart +++ b/packages/ferrostar_flutter/example/lib/main.dart @@ -1,7 +1,8 @@ import 'dart:convert'; + +import 'package:ferrostar_flutter/ferrostar_flutter.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import 'package:ferrostar_flutter/ferrostar_flutter.dart'; void main() => runApp(const MaterialApp(home: E2EHome())); @@ -39,7 +40,7 @@ class _E2EHomeState extends State { _ctrl = ctrl; _log = 'Controller ready'; }); - } catch (e) { + } on Object catch (e) { setState(() => _log = 'Error: $e'); } finally { setState(() => _loading = false); @@ -51,11 +52,11 @@ class _E2EHomeState extends State { await _ctrl?.updateLocation(UserLocation( lat: 59.4429, lng: 24.7653, - horizontalAccuracyM: 5.0, - courseDeg: 315.0, + horizontalAccuracyM: 5, + courseDeg: 315, timestampMs: DateTime.now().millisecondsSinceEpoch, )); - } catch (e) { + } on Object catch (e) { setState(() => _log = 'Tick error: $e'); } } diff --git a/packages/ferrostar_flutter/example/pubspec.lock b/packages/ferrostar_flutter/example/pubspec.lock index 65042c3..5f24bca 100644 --- a/packages/ferrostar_flutter/example/pubspec.lock +++ b/packages/ferrostar_flutter/example/pubspec.lock @@ -278,6 +278,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.2.0" + very_good_analysis: + dependency: "direct dev" + description: + name: very_good_analysis + sha256: d1cb1d66a5aae2c702d68caca6c8347306d35e728fd94555fa21fa0448a972e0 + url: "https://pub.dev" + source: hosted + version: "10.2.0" vm_service: dependency: transitive description: diff --git a/packages/ferrostar_flutter/example/pubspec.yaml b/packages/ferrostar_flutter/example/pubspec.yaml index 8869f11..d4a1659 100644 --- a/packages/ferrostar_flutter/example/pubspec.yaml +++ b/packages/ferrostar_flutter/example/pubspec.yaml @@ -14,8 +14,9 @@ environment: # the latest version available on pub.dev. To see which dependencies have newer # versions available, run `flutter pub outdated`. dependencies: - flutter: - sdk: flutter + # The following adds the Cupertino Icons font to your application. + # Use with the CupertinoIcons class for iOS style icons. + cupertino_icons: ^1.0.8 ferrostar_flutter: # When depending on this package from a real application you should use: @@ -25,22 +26,22 @@ dependencies: # the parent directory to use the current plugin's version. path: ../ - # The following adds the Cupertino Icons font to your application. - # Use with the CupertinoIcons class for iOS style icons. - cupertino_icons: ^1.0.8 - -dev_dependencies: - integration_test: - sdk: flutter - flutter_test: + flutter: sdk: flutter +dev_dependencies: # The "flutter_lints" package below contains a set of recommended lints to # encourage good coding practices. The lint set provided by the package is # activated in the `analysis_options.yaml` file located at the root of your # package. See that file for information about deactivating specific lint # rules and activating additional ones. flutter_lints: ^6.0.0 + flutter_test: + sdk: flutter + integration_test: + sdk: flutter + + very_good_analysis: ^10.2.0 # For information on the generic Dart part of this file, see the # following page: https://dart.dev/tools/pub/pubspec diff --git a/packages/ferrostar_flutter/example/test/widget_test.dart b/packages/ferrostar_flutter/example/test/widget_test.dart index 7f8f990..1277f73 100644 --- a/packages/ferrostar_flutter/example/test/widget_test.dart +++ b/packages/ferrostar_flutter/example/test/widget_test.dart @@ -1,10 +1,9 @@ +import 'package:ferrostar_flutter_example/main.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:ferrostar_flutter_example/main.dart'; - void main() { - testWidgets('E2EHome renders without error', (WidgetTester tester) async { + testWidgets('E2EHome renders without error', (tester) async { await tester.pumpWidget(const MaterialApp(home: E2EHome())); expect(find.byType(Scaffold), findsOneWidget); }); diff --git a/packages/ferrostar_flutter/lib/src/controller.dart b/packages/ferrostar_flutter/lib/src/controller.dart index 5d07979..953f458 100644 --- a/packages/ferrostar_flutter/lib/src/controller.dart +++ b/packages/ferrostar_flutter/lib/src/controller.dart @@ -1,37 +1,65 @@ -import 'ferrostar_flutter_platform.dart'; -import 'models/navigation_state.dart'; -import 'models/route_deviation.dart'; -import 'models/spoken_instruction.dart'; -import 'models/user_location.dart'; +import 'package:ferrostar_flutter/src/ferrostar_flutter_platform.dart'; +import 'package:ferrostar_flutter/src/models/navigation_state.dart'; +import 'package:ferrostar_flutter/src/models/route_deviation.dart'; +import 'package:ferrostar_flutter/src/models/spoken_instruction.dart'; +import 'package:ferrostar_flutter/src/models/user_location.dart'; +/// Handle to a single navigation session managed by the native Ferrostar core. +/// +/// Obtained from `FerrostarFlutter.createController`. Each instance owns a +/// platform-side controller identified by [id]; you must call [dispose] when +/// done so the native resources (location subscriptions, event channels) are +/// released. Once disposed, further calls throw [StateError]. class FerrostarController { + /// Wraps a platform-allocated controller with the given [id], routing + /// subsequent calls through [_platform]. Normally constructed by + /// `FerrostarFlutter.createController`. FerrostarController(this._id, this._platform); final String _id; final FerrostarFlutterPlatform _platform; bool _disposed = false; + /// Opaque platform-side controller identifier. Stable for the lifetime of + /// this instance; useful for logging and correlating native traces. String get id => _id; + /// Stream of navigation state snapshots (status, snapped location, current + /// step, progress, visual instruction). Emits whenever the core re-evaluates + /// state — typically after each [updateLocation] call. Stream get stateStream => _platform.stateStream(controllerId: _id); + /// Stream of spoken instructions that have crossed their trigger distance. + /// Each event represents a single utterance to be passed to a TTS engine. Stream get spokenInstructionStream => _platform.spokenInstructionStream(controllerId: _id); + /// Stream of route-deviation events. Emits when the core determines the + /// user has been off-route beyond the configured distance and time + /// thresholds; consumers typically respond by requesting a new route. Stream get deviationStream => _platform.deviationStream(controllerId: _id); + /// Pushes a new GPS fix into the navigation core. Drives all downstream + /// state and event streams. Throws [StateError] if the controller has + /// already been disposed. Future updateLocation(UserLocation location) { _requireAlive(); return _platform.updateLocation(controllerId: _id, location: location); } + /// Replaces the active route with a freshly fetched OSRM response (e.g. + /// after a deviation). The current location and progress are preserved + /// where possible. Throws [StateError] if disposed. Future replaceRoute(Map osrmJson) { _requireAlive(); return _platform.replaceRoute(controllerId: _id, osrmJson: osrmJson); } + /// Tears down the platform-side controller and closes its event channels. + /// Idempotent — calling multiple times is safe but only the first call + /// performs work. Future dispose() async { if (_disposed) return; _disposed = true; diff --git a/packages/ferrostar_flutter/lib/src/exceptions.dart b/packages/ferrostar_flutter/lib/src/exceptions.dart index 1ac7f7e..5c02ac9 100644 --- a/packages/ferrostar_flutter/lib/src/exceptions.dart +++ b/packages/ferrostar_flutter/lib/src/exceptions.dart @@ -1,23 +1,48 @@ +/// Base class for all errors surfaced by the Ferrostar plugin. +/// +/// Wraps a stable [code] string (matching the platform-channel error codes +/// emitted by the native side) and a human-readable [message]. Catch this to +/// handle every plugin-originated failure uniformly. class FerrostarException implements Exception { + /// Creates an exception with a platform-channel [code] and [message]. + FerrostarException(this.code, this.message); + + /// Stable identifier matching the native error code (e.g. + /// `invalid_argument`, `route_parse_failed`). Suitable for programmatic + /// dispatch. final String code; + + /// Human-readable description of what went wrong. final String message; - FerrostarException(this.code, this.message); @override String toString() => 'FerrostarException($code): $message'; } +/// Thrown when an argument fails validation on the native side (bad +/// coordinates, malformed config, etc.). class InvalidArgumentException extends FerrostarException { + /// Creates the exception with the explanatory [message]. InvalidArgumentException(String message) : super('invalid_argument', message); } +/// Thrown when an OSRM JSON payload could not be decoded into a route. class RouteParseException extends FerrostarException { + /// Creates the exception with the explanatory [message]. RouteParseException(String message) : super('route_parse_failed', message); } +/// Thrown when an operation references a controller id that has already been +/// disposed or never existed on the native side. class UnknownControllerException extends FerrostarException { - UnknownControllerException(String message) : super('unknown_controller', message); + /// Creates the exception with the explanatory [message]. + UnknownControllerException(String message) + : super('unknown_controller', message); } +/// Thrown for unexpected internal errors from the Ferrostar core (bug, +/// invariant violation, OS-level failure). Should be rare in practice. class FerrostarInternalException extends FerrostarException { - FerrostarInternalException(String message) : super('ferrostar_error', message); + /// Creates the exception with the explanatory [message]. + FerrostarInternalException(String message) + : super('ferrostar_error', message); } diff --git a/packages/ferrostar_flutter/lib/src/ferrostar_flutter.dart b/packages/ferrostar_flutter/lib/src/ferrostar_flutter.dart index 12f4f9f..032ab04 100644 --- a/packages/ferrostar_flutter/lib/src/ferrostar_flutter.dart +++ b/packages/ferrostar_flutter/lib/src/ferrostar_flutter.dart @@ -1,12 +1,25 @@ -import 'controller.dart'; -import 'ferrostar_flutter_platform.dart'; -import 'models/navigation_config.dart'; -import 'models/waypoint_input.dart'; +import 'package:ferrostar_flutter/src/controller.dart'; +import 'package:ferrostar_flutter/src/ferrostar_flutter_platform.dart'; +import 'package:ferrostar_flutter/src/models/navigation_config.dart'; +import 'package:ferrostar_flutter/src/models/waypoint_input.dart'; +/// Entry point for the Ferrostar Flutter plugin. +/// +/// Use [FerrostarFlutter.instance] to access the singleton facade and +/// [createController] to start a navigation session. Each session is owned by +/// a [FerrostarController] and must be disposed when no longer needed. class FerrostarFlutter { FerrostarFlutter._(); + + /// Singleton facade; routes calls to the active platform implementation. static final FerrostarFlutter instance = FerrostarFlutter._(); + /// Allocates a navigation controller on the native side from a precomputed + /// OSRM route ([osrmJson]) and the [waypoints] used to request it. + /// + /// The returned [FerrostarController] owns native resources — call + /// [FerrostarController.dispose] when finished. [config] is optional and + /// defaults to the standard deviation thresholds. Future createController({ required Map osrmJson, required List waypoints, diff --git a/packages/ferrostar_flutter/lib/src/ferrostar_flutter_platform.dart b/packages/ferrostar_flutter/lib/src/ferrostar_flutter_platform.dart index ec6b931..e199163 100644 --- a/packages/ferrostar_flutter/lib/src/ferrostar_flutter_platform.dart +++ b/packages/ferrostar_flutter/lib/src/ferrostar_flutter_platform.dart @@ -1,42 +1,64 @@ +import 'package:ferrostar_flutter/src/method_channel_platform.dart'; +import 'package:ferrostar_flutter/src/models/navigation_config.dart'; +import 'package:ferrostar_flutter/src/models/navigation_state.dart'; +import 'package:ferrostar_flutter/src/models/route_deviation.dart'; +import 'package:ferrostar_flutter/src/models/spoken_instruction.dart'; +import 'package:ferrostar_flutter/src/models/user_location.dart'; +import 'package:ferrostar_flutter/src/models/waypoint_input.dart'; import 'package:plugin_platform_interface/plugin_platform_interface.dart'; -import 'models/navigation_config.dart'; -import 'models/navigation_state.dart'; -import 'models/route_deviation.dart'; -import 'models/spoken_instruction.dart'; -import 'models/user_location.dart'; -import 'models/waypoint_input.dart'; -import 'method_channel_platform.dart'; +/// Platform interface for the Ferrostar plugin. +/// +/// Concrete implementations (the bundled [MethodChannelFerrostarFlutter] for +/// real platforms, fakes in tests) extend this class. Replace +/// [FerrostarFlutterPlatform.instance] in tests to mock the entire native +/// boundary. abstract class FerrostarFlutterPlatform extends PlatformInterface { + /// Subclass constructor; passes a token used to verify subclasses. FerrostarFlutterPlatform() : super(token: _token); static final Object _token = Object(); static FerrostarFlutterPlatform _instance = MethodChannelFerrostarFlutter(); + + /// The active platform implementation. Defaults to the method-channel + /// backend; tests replace it with a fake. static FerrostarFlutterPlatform get instance => _instance; static set instance(FerrostarFlutterPlatform inst) { PlatformInterface.verifyToken(inst, _token); _instance = inst; } + /// Creates a native controller for the given OSRM route and waypoints, and + /// returns its opaque id. Future createController({ required Map osrmJson, required List waypoints, required NavigationConfig config, }); + /// Pushes a new GPS fix into the controller identified by [controllerId]. Future updateLocation({ required String controllerId, required UserLocation location, }); + /// Replaces the active route on the controller identified by [controllerId]. Future replaceRoute({ required String controllerId, required Map osrmJson, }); + /// Releases the native controller identified by [controllerId]. Future dispose({required String controllerId}); + /// Returns the navigation-state stream for [controllerId]. Stream stateStream({required String controllerId}); - Stream spokenInstructionStream({required String controllerId}); + + /// Returns the spoken-instruction stream for [controllerId]. + Stream spokenInstructionStream({ + required String controllerId, + }); + + /// Returns the route-deviation stream for [controllerId]. Stream deviationStream({required String controllerId}); } diff --git a/packages/ferrostar_flutter/lib/src/method_channel_platform.dart b/packages/ferrostar_flutter/lib/src/method_channel_platform.dart index 94bdf9d..06f2128 100644 --- a/packages/ferrostar_flutter/lib/src/method_channel_platform.dart +++ b/packages/ferrostar_flutter/lib/src/method_channel_platform.dart @@ -1,17 +1,19 @@ import 'dart:async'; + +import 'package:ferrostar_flutter/src/ferrostar_flutter_platform.dart'; +import 'package:ferrostar_flutter/src/models/navigation_config.dart'; +import 'package:ferrostar_flutter/src/models/navigation_state.dart'; +import 'package:ferrostar_flutter/src/models/route_deviation.dart'; +import 'package:ferrostar_flutter/src/models/spoken_instruction.dart'; +import 'package:ferrostar_flutter/src/models/user_location.dart'; +import 'package:ferrostar_flutter/src/models/waypoint_input.dart'; import 'package:flutter/services.dart'; -import 'ferrostar_flutter_platform.dart'; -import 'models/navigation_config.dart'; -import 'models/navigation_state.dart'; -import 'models/route_deviation.dart'; -import 'models/spoken_instruction.dart'; -import 'models/user_location.dart'; -import 'models/waypoint_input.dart'; const _channelName = 'land._001/ferrostar_flutter'; -/// Recursively converts Map → Map and -/// List elements, so platform-channel payloads are safe for fromJson. +/// Recursively converts `Map` to `Map` and +/// normalizes nested list elements, so platform-channel payloads are safe to +/// pass into freezed `fromJson` constructors. dynamic _deepNormalize(dynamic value) { if (value is Map) { return Map.fromEntries( @@ -26,6 +28,9 @@ dynamic _deepNormalize(dynamic value) { return value; } +/// Default [FerrostarFlutterPlatform] implementation that talks to the native +/// side over Flutter platform channels (one [MethodChannel] for RPC calls and +/// per-controller [EventChannel]s for state/spoken/deviation streams). class MethodChannelFerrostarFlutter extends FerrostarFlutterPlatform { final MethodChannel _channel = const MethodChannel(_channelName); @@ -67,30 +72,40 @@ class MethodChannelFerrostarFlutter extends FerrostarFlutterPlatform { @override Future dispose({required String controllerId}) async { - await _channel.invokeMethod('dispose', {'controller_id': controllerId}); + await _channel.invokeMethod('dispose', { + 'controller_id': controllerId, + }); } @override Stream stateStream({required String controllerId}) { final ch = EventChannel('$_channelName/state/$controllerId'); - return ch - .receiveBroadcastStream() - .map((e) => NavigationState.fromJson(_deepNormalize(e) as Map)); + return ch.receiveBroadcastStream().map( + (e) => NavigationState.fromJson( + _deepNormalize(e) as Map, + ), + ); } @override - Stream spokenInstructionStream({required String controllerId}) { + Stream spokenInstructionStream({ + required String controllerId, + }) { final ch = EventChannel('$_channelName/spoken/$controllerId'); - return ch - .receiveBroadcastStream() - .map((e) => SpokenInstruction.fromJson(_deepNormalize(e) as Map)); + return ch.receiveBroadcastStream().map( + (e) => SpokenInstruction.fromJson( + _deepNormalize(e) as Map, + ), + ); } @override Stream deviationStream({required String controllerId}) { final ch = EventChannel('$_channelName/deviation/$controllerId'); - return ch - .receiveBroadcastStream() - .map((e) => RouteDeviation.fromJson(_deepNormalize(e) as Map)); + return ch.receiveBroadcastStream().map( + (e) => RouteDeviation.fromJson( + _deepNormalize(e) as Map, + ), + ); } } diff --git a/packages/ferrostar_flutter/lib/src/models/navigation_config.dart b/packages/ferrostar_flutter/lib/src/models/navigation_config.dart index 4c41597..9cd36ef 100644 --- a/packages/ferrostar_flutter/lib/src/models/navigation_config.dart +++ b/packages/ferrostar_flutter/lib/src/models/navigation_config.dart @@ -3,14 +3,33 @@ import 'package:freezed_annotation/freezed_annotation.dart'; part 'navigation_config.freezed.dart'; part 'navigation_config.g.dart'; +/// Tunable thresholds for the Ferrostar navigation core. Defaults match the +/// upstream library; override only when you understand the tradeoffs. @freezed class NavigationConfig with _$NavigationConfig { + /// Creates a [NavigationConfig]. const factory NavigationConfig({ - @JsonKey(name: 'deviation_threshold_m') @Default(50.0) double deviationThresholdM, - @JsonKey(name: 'deviation_duration_threshold_ms') @Default(10000) int deviationDurationThresholdMs, - @JsonKey(name: 'snap_user_location_to_route') @Default(true) bool snapUserLocationToRoute, + /// Distance in meters the user may stray from the route before being + /// considered off-route. + @JsonKey(name: 'deviation_threshold_m') + @Default(50.0) + double deviationThresholdM, + + /// Time in milliseconds the user must remain past + /// [deviationThresholdM] before a deviation event is emitted. Filters + /// transient GPS noise. + @JsonKey(name: 'deviation_duration_threshold_ms') + @Default(10000) + int deviationDurationThresholdMs, + + /// When true, snap reported user locations onto the polyline before + /// updating progress and emitting state. + @JsonKey(name: 'snap_user_location_to_route') + @Default(true) + bool snapUserLocationToRoute, }) = _NavigationConfig; + /// Decodes a config from its JSON representation. factory NavigationConfig.fromJson(Map json) => _$NavigationConfigFromJson(json); } diff --git a/packages/ferrostar_flutter/lib/src/models/navigation_state.dart b/packages/ferrostar_flutter/lib/src/models/navigation_state.dart index 9da2c81..c20fc1e 100644 --- a/packages/ferrostar_flutter/lib/src/models/navigation_state.dart +++ b/packages/ferrostar_flutter/lib/src/models/navigation_state.dart @@ -1,39 +1,71 @@ +import 'package:ferrostar_flutter/src/models/trip_progress.dart'; +import 'package:ferrostar_flutter/src/models/user_location.dart'; +import 'package:ferrostar_flutter/src/models/visual_instruction.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; -import 'user_location.dart'; -import 'trip_progress.dart'; -import 'visual_instruction.dart'; part 'navigation_state.freezed.dart'; part 'navigation_state.g.dart'; +/// High-level lifecycle of a navigation session. enum TripStatus { - @JsonValue('idle') idle, - @JsonValue('navigating') navigating, - @JsonValue('complete') complete, + /// No active trip — controller created but no location updates yet. + @JsonValue('idle') + idle, + + /// Trip in progress; the user is moving toward the destination. + @JsonValue('navigating') + navigating, + + /// Final waypoint reached; the trip has finished. + @JsonValue('complete') + complete, } +/// Lightweight reference to the route step the user is currently on. @freezed class StepRef with _$StepRef { + /// Creates a [StepRef]. const factory StepRef({ - int? index, + /// Human-readable name of the road for the current step. @JsonKey(name: 'road_name') required String roadName, + + /// Zero-based index of the step within the active route, when known. + int? index, }) = _StepRef; + /// Decodes a step reference from its JSON representation. factory StepRef.fromJson(Map json) => _$StepRefFromJson(json); } +/// Snapshot of the navigation core's current state. Emitted on +/// `FerrostarController.stateStream` after each location update. @freezed class NavigationState with _$NavigationState { + /// Creates a [NavigationState]. const factory NavigationState({ + /// Current trip lifecycle status. required TripStatus status, + + /// True while the user is past the deviation thresholds set in + /// `NavigationConfig`. @JsonKey(name: 'is_off_route') required bool isOffRoute, + + /// User location snapped to the route polyline (null if snapping is + /// disabled or no fix has been pushed yet). @JsonKey(name: 'snapped_location') UserLocation? snappedLocation, + + /// Distance/time progress for the trip, when available. TripProgress? progress, + + /// Visual instruction for the upcoming maneuver, if any. @JsonKey(name: 'current_visual') VisualInstruction? currentVisual, + + /// Reference to the step the user is currently on, if any. @JsonKey(name: 'current_step') StepRef? currentStep, }) = _NavigationState; + /// Decodes a navigation state from its JSON representation. factory NavigationState.fromJson(Map json) => _$NavigationStateFromJson(json); } diff --git a/packages/ferrostar_flutter/lib/src/models/route_deviation.dart b/packages/ferrostar_flutter/lib/src/models/route_deviation.dart index eaa93d3..afe6f3b 100644 --- a/packages/ferrostar_flutter/lib/src/models/route_deviation.dart +++ b/packages/ferrostar_flutter/lib/src/models/route_deviation.dart @@ -1,17 +1,28 @@ +import 'package:ferrostar_flutter/src/models/user_location.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; -import 'user_location.dart'; part 'route_deviation.freezed.dart'; part 'route_deviation.g.dart'; +/// Event emitted when the user has been off-route past the deviation +/// thresholds. Consumers typically respond by requesting a fresh route. @freezed class RouteDeviation with _$RouteDeviation { + /// Creates a [RouteDeviation]. const factory RouteDeviation({ + /// Perpendicular distance in meters from the user's location to the + /// route polyline at detection time. @JsonKey(name: 'deviation_m') required double deviationM, + + /// How long the user has been past the deviation distance, in + /// milliseconds. @JsonKey(name: 'duration_off_route_ms') required int durationOffRouteMs, + + /// User location at the moment the deviation was detected. @JsonKey(name: 'user_location') required UserLocation userLocation, }) = _RouteDeviation; + /// Decodes a deviation event from its JSON representation. factory RouteDeviation.fromJson(Map json) => _$RouteDeviationFromJson(json); } diff --git a/packages/ferrostar_flutter/lib/src/models/spoken_instruction.dart b/packages/ferrostar_flutter/lib/src/models/spoken_instruction.dart index ce601cb..540ce38 100644 --- a/packages/ferrostar_flutter/lib/src/models/spoken_instruction.dart +++ b/packages/ferrostar_flutter/lib/src/models/spoken_instruction.dart @@ -3,16 +3,31 @@ import 'package:freezed_annotation/freezed_annotation.dart'; part 'spoken_instruction.freezed.dart'; part 'spoken_instruction.g.dart'; +/// A single voice prompt that has crossed its trigger distance and should be +/// spoken to the user (typically via a TTS engine). @freezed class SpokenInstruction with _$SpokenInstruction { + /// Creates a [SpokenInstruction]. const factory SpokenInstruction({ + /// Stable identifier for this instruction, used to deduplicate replays. required String uuid, + + /// Plain-text utterance to speak. required String text, - String? ssml, + + /// Distance to the maneuver, in meters, at which this instruction was + /// scheduled to be emitted. @JsonKey(name: 'trigger_distance_m') required double triggerDistanceM, + + /// Wall-clock time the native side emitted this instruction + /// (milliseconds since epoch). @JsonKey(name: 'emitted_at_ms') required int emittedAtMs, + + /// Optional SSML form of [text], for engines that support markup. + String? ssml, }) = _SpokenInstruction; + /// Decodes a spoken instruction from its JSON representation. factory SpokenInstruction.fromJson(Map json) => _$SpokenInstructionFromJson(json); } diff --git a/packages/ferrostar_flutter/lib/src/models/trip_progress.dart b/packages/ferrostar_flutter/lib/src/models/trip_progress.dart index c01961c..0728473 100644 --- a/packages/ferrostar_flutter/lib/src/models/trip_progress.dart +++ b/packages/ferrostar_flutter/lib/src/models/trip_progress.dart @@ -3,14 +3,24 @@ import 'package:freezed_annotation/freezed_annotation.dart'; part 'trip_progress.freezed.dart'; part 'trip_progress.g.dart'; +/// Distance and time progress for the active trip, as computed by the +/// navigation core. @freezed class TripProgress with _$TripProgress { + /// Creates a [TripProgress]. const factory TripProgress({ - @JsonKey(name: 'distance_to_next_maneuver_m') required double distanceToNextManeuverM, + /// Distance, in meters, from the user to the next maneuver. + @JsonKey(name: 'distance_to_next_maneuver_m') + required double distanceToNextManeuverM, + + /// Total remaining distance to the destination, in meters. @JsonKey(name: 'distance_remaining_m') required double distanceRemainingM, + + /// Estimated remaining trip duration, in milliseconds. @JsonKey(name: 'duration_remaining_ms') required int durationRemainingMs, }) = _TripProgress; + /// Decodes a trip progress snapshot from its JSON representation. factory TripProgress.fromJson(Map json) => _$TripProgressFromJson(json); } diff --git a/packages/ferrostar_flutter/lib/src/models/user_location.dart b/packages/ferrostar_flutter/lib/src/models/user_location.dart index 8478635..675d3a7 100644 --- a/packages/ferrostar_flutter/lib/src/models/user_location.dart +++ b/packages/ferrostar_flutter/lib/src/models/user_location.dart @@ -3,17 +3,32 @@ import 'package:freezed_annotation/freezed_annotation.dart'; part 'user_location.freezed.dart'; part 'user_location.g.dart'; +/// A single GPS fix passed into the navigation core. @freezed class UserLocation with _$UserLocation { + /// Creates a [UserLocation]. const factory UserLocation({ + /// Latitude in WGS84 degrees. required double lat, + + /// Longitude in WGS84 degrees. required double lng, - @JsonKey(name: 'horizontal_accuracy_m') required double horizontalAccuracyM, + + /// Reported horizontal accuracy of the fix, in meters (1-sigma). + @JsonKey(name: 'horizontal_accuracy_m') + required double horizontalAccuracyM, + + /// Wall-clock time of the fix, in milliseconds since epoch. + @JsonKey(name: 'timestamp_ms') required int timestampMs, + + /// Course over ground in degrees clockwise from true north, when known. @JsonKey(name: 'course_deg') double? courseDeg, + + /// Speed over ground in meters per second, when known. @JsonKey(name: 'speed_mps') double? speedMps, - @JsonKey(name: 'timestamp_ms') required int timestampMs, }) = _UserLocation; + /// Decodes a user location from its JSON representation. factory UserLocation.fromJson(Map json) => _$UserLocationFromJson(json); } diff --git a/packages/ferrostar_flutter/lib/src/models/visual_instruction.dart b/packages/ferrostar_flutter/lib/src/models/visual_instruction.dart index da75d2b..e165625 100644 --- a/packages/ferrostar_flutter/lib/src/models/visual_instruction.dart +++ b/packages/ferrostar_flutter/lib/src/models/visual_instruction.dart @@ -3,16 +3,29 @@ import 'package:freezed_annotation/freezed_annotation.dart'; part 'visual_instruction.freezed.dart'; part 'visual_instruction.g.dart'; +/// On-screen banner instruction for the upcoming maneuver. Mirrors the +/// Mapbox/OSRM "banner" model used by Ferrostar. @freezed class VisualInstruction with _$VisualInstruction { + /// Creates a [VisualInstruction]. const factory VisualInstruction({ + /// Primary banner text (e.g. the next street name). @JsonKey(name: 'primary_text') required String primaryText, - @JsonKey(name: 'secondary_text') String? secondaryText, + + /// Maneuver type token (e.g. `turn`, `merge`, `roundabout`). @JsonKey(name: 'maneuver_type') required String maneuverType, - @JsonKey(name: 'maneuver_modifier') String? maneuverModifier, + + /// Distance, in meters, at which this banner becomes active. @JsonKey(name: 'trigger_distance_m') required double triggerDistanceM, + + /// Optional secondary banner text (e.g. lane guidance or a sub-name). + @JsonKey(name: 'secondary_text') String? secondaryText, + + /// Optional modifier on the maneuver (e.g. `left`, `slight right`). + @JsonKey(name: 'maneuver_modifier') String? maneuverModifier, }) = _VisualInstruction; + /// Decodes a visual instruction from its JSON representation. factory VisualInstruction.fromJson(Map json) => _$VisualInstructionFromJson(json); } diff --git a/packages/ferrostar_flutter/lib/src/models/waypoint_input.dart b/packages/ferrostar_flutter/lib/src/models/waypoint_input.dart index 174c4d6..d95e3a6 100644 --- a/packages/ferrostar_flutter/lib/src/models/waypoint_input.dart +++ b/packages/ferrostar_flutter/lib/src/models/waypoint_input.dart @@ -3,19 +3,39 @@ import 'package:freezed_annotation/freezed_annotation.dart'; part 'waypoint_input.freezed.dart'; part 'waypoint_input.g.dart'; +/// How the navigation core should treat a waypoint when matching progress +/// and emitting completion. enum WaypointKind { - @JsonValue('break') breakPoint, - @JsonValue('via_point') viaPoint, + /// A stop the user must arrive at; reaching it advances the trip and + /// (for the last one) completes navigation. + @JsonValue('break') + breakPoint, + + /// An intermediate point the route should pass through but not "arrive" + /// at; useful for shaping routes without visible stops. + @JsonValue('via_point') + viaPoint, } +/// Input description of a waypoint passed to the navigation core when +/// creating a controller. Note this is the *input* — distinct from any +/// internal waypoint representation in the core. @freezed class WaypointInput with _$WaypointInput { + /// Creates a [WaypointInput]. const factory WaypointInput({ + /// Latitude in WGS84 degrees. required double lat, + + /// Longitude in WGS84 degrees. required double lng, + + /// How the core should treat this waypoint. Defaults to + /// [WaypointKind.breakPoint]. @Default(WaypointKind.breakPoint) WaypointKind kind, }) = _WaypointInput; + /// Decodes a waypoint input from its JSON representation. factory WaypointInput.fromJson(Map json) => _$WaypointInputFromJson(json); } diff --git a/packages/ferrostar_flutter/pubspec.yaml b/packages/ferrostar_flutter/pubspec.yaml index 6919f2a..436c522 100644 --- a/packages/ferrostar_flutter/pubspec.yaml +++ b/packages/ferrostar_flutter/pubspec.yaml @@ -11,17 +11,18 @@ environment: dependencies: flutter: sdk: flutter - plugin_platform_interface: ^2.1.8 freezed_annotation: ^2.4.1 json_annotation: ^4.9.0 + plugin_platform_interface: ^2.1.8 dev_dependencies: + build_runner: ^2.4.11 + flutter_lints: ^4.0.0 flutter_test: sdk: flutter - flutter_lints: ^4.0.0 - build_runner: ^2.4.11 freezed: ^2.5.7 json_serializable: ^6.8.0 + very_good_analysis: ^10.2.0 flutter: plugin: diff --git a/packages/ferrostar_flutter/test/controller_test.dart b/packages/ferrostar_flutter/test/controller_test.dart index 31d0322..6b119db 100644 --- a/packages/ferrostar_flutter/test/controller_test.dart +++ b/packages/ferrostar_flutter/test/controller_test.dart @@ -1,7 +1,8 @@ import 'dart:async'; -import 'package:flutter_test/flutter_test.dart'; + import 'package:ferrostar_flutter/ferrostar_flutter.dart'; import 'package:ferrostar_flutter/src/ferrostar_flutter_platform.dart'; +import 'package:flutter_test/flutter_test.dart'; class _FakePlatform extends FerrostarFlutterPlatform { String? lastCall; @@ -65,7 +66,7 @@ void main() { test('createController goes through facade', () async { final c = await FerrostarFlutter.instance.createController( - osrmJson: {'code': 'Ok', 'routes': []}, + osrmJson: {'code': 'Ok', 'routes': []}, waypoints: [const WaypointInput(lat: 52.52, lng: 13.405)], ); expect(c.id, 'test-id'); @@ -74,7 +75,7 @@ void main() { test('updateLocation after dispose throws StateError', () async { final c = await FerrostarFlutter.instance.createController( - osrmJson: {'code': 'Ok', 'routes': []}, + osrmJson: {'code': 'Ok', 'routes': []}, waypoints: [const WaypointInput(lat: 52.52, lng: 13.405)], ); await c.dispose(); diff --git a/packages/ferrostar_flutter/test/method_channel_platform_test.dart b/packages/ferrostar_flutter/test/method_channel_platform_test.dart index dc20f25..d3b4c3d 100644 --- a/packages/ferrostar_flutter/test/method_channel_platform_test.dart +++ b/packages/ferrostar_flutter/test/method_channel_platform_test.dart @@ -1,9 +1,9 @@ -import 'package:flutter/services.dart'; -import 'package:flutter_test/flutter_test.dart'; import 'package:ferrostar_flutter/src/method_channel_platform.dart'; import 'package:ferrostar_flutter/src/models/navigation_config.dart'; import 'package:ferrostar_flutter/src/models/user_location.dart'; import 'package:ferrostar_flutter/src/models/waypoint_input.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; void main() { TestWidgetsFlutterBinding.ensureInitialized(); @@ -38,7 +38,7 @@ void main() { test('createController sends osrm_json, waypoints, config', () async { final id = await platform.createController( - osrmJson: {'code': 'Ok', 'routes': []}, + osrmJson: {'code': 'Ok', 'routes': []}, waypoints: [ const WaypointInput(lat: 52.52, lng: 13.405), const WaypointInput(lat: 52.50, lng: 13.40), @@ -50,7 +50,7 @@ void main() { expect(log.first.method, 'createController'); final args = log.first.arguments as Map; expect(args['osrm_json'], isA>()); - expect((args['waypoints'] as List), hasLength(2)); + expect(args['waypoints'] as List, hasLength(2)); expect(args['config'], isA>()); }); @@ -60,7 +60,7 @@ void main() { location: const UserLocation( lat: 52.52, lng: 13.405, - horizontalAccuracyM: 5.0, + horizontalAccuracyM: 5, timestampMs: 1, ), ); @@ -73,7 +73,7 @@ void main() { test('replaceRoute sends controller_id and osrm_json', () async { await platform.replaceRoute( controllerId: 'ctrl-1', - osrmJson: {'code': 'Ok', 'routes': []}, + osrmJson: {'code': 'Ok', 'routes': []}, ); expect(log.single.method, 'replaceRoute'); }); diff --git a/packages/ferrostar_flutter/test/models/inputs_test.dart b/packages/ferrostar_flutter/test/models/inputs_test.dart index 650f7af..3e53d04 100644 --- a/packages/ferrostar_flutter/test/models/inputs_test.dart +++ b/packages/ferrostar_flutter/test/models/inputs_test.dart @@ -1,16 +1,22 @@ -import 'package:flutter_test/flutter_test.dart'; -import 'package:ferrostar_flutter/src/models/waypoint_input.dart'; import 'package:ferrostar_flutter/src/models/navigation_config.dart'; +import 'package:ferrostar_flutter/src/models/waypoint_input.dart'; +import 'package:flutter_test/flutter_test.dart'; void main() { test('WaypointInput serializes', () { - const w = WaypointInput(lat: 52.52, lng: 13.405, kind: WaypointKind.viaPoint); + const w = WaypointInput( + lat: 52.52, + lng: 13.405, + kind: WaypointKind.viaPoint, + ); expect(w.toJson(), {'lat': 52.52, 'lng': 13.405, 'kind': 'via_point'}); }); test('WaypointInput kinds', () { - expect(WaypointInput.fromJson({'lat': 0.0, 'lng': 0.0, 'kind': 'break'}).kind, - WaypointKind.breakPoint); + expect( + WaypointInput.fromJson({'lat': 0.0, 'lng': 0.0, 'kind': 'break'}).kind, + WaypointKind.breakPoint, + ); }); test('NavigationConfig default serializes with sane defaults', () { diff --git a/packages/ferrostar_flutter/test/models/navigation_state_test.dart b/packages/ferrostar_flutter/test/models/navigation_state_test.dart index 786c614..8638c2f 100644 --- a/packages/ferrostar_flutter/test/models/navigation_state_test.dart +++ b/packages/ferrostar_flutter/test/models/navigation_state_test.dart @@ -1,5 +1,5 @@ -import 'package:flutter_test/flutter_test.dart'; import 'package:ferrostar_flutter/src/models/navigation_state.dart'; +import 'package:flutter_test/flutter_test.dart'; void main() { group('NavigationState', () { diff --git a/packages/ferrostar_flutter/test/models/route_deviation_test.dart b/packages/ferrostar_flutter/test/models/route_deviation_test.dart index 25acd73..65b4855 100644 --- a/packages/ferrostar_flutter/test/models/route_deviation_test.dart +++ b/packages/ferrostar_flutter/test/models/route_deviation_test.dart @@ -1,5 +1,5 @@ -import 'package:flutter_test/flutter_test.dart'; import 'package:ferrostar_flutter/src/models/route_deviation.dart'; +import 'package:flutter_test/flutter_test.dart'; void main() { test('RouteDeviation round-trips', () { diff --git a/packages/ferrostar_flutter/test/models/spoken_instruction_test.dart b/packages/ferrostar_flutter/test/models/spoken_instruction_test.dart index 932f26d..81de6ae 100644 --- a/packages/ferrostar_flutter/test/models/spoken_instruction_test.dart +++ b/packages/ferrostar_flutter/test/models/spoken_instruction_test.dart @@ -1,5 +1,5 @@ -import 'package:flutter_test/flutter_test.dart'; import 'package:ferrostar_flutter/src/models/spoken_instruction.dart'; +import 'package:flutter_test/flutter_test.dart'; void main() { test('SpokenInstruction round-trips with ssml', () { diff --git a/packages/ferrostar_flutter/test/models/trip_progress_test.dart b/packages/ferrostar_flutter/test/models/trip_progress_test.dart index 77e677e..6503707 100644 --- a/packages/ferrostar_flutter/test/models/trip_progress_test.dart +++ b/packages/ferrostar_flutter/test/models/trip_progress_test.dart @@ -1,5 +1,5 @@ -import 'package:flutter_test/flutter_test.dart'; import 'package:ferrostar_flutter/src/models/trip_progress.dart'; +import 'package:flutter_test/flutter_test.dart'; void main() { test('TripProgress round-trips', () { diff --git a/packages/ferrostar_flutter/test/models/user_location_test.dart b/packages/ferrostar_flutter/test/models/user_location_test.dart index 84dcb27..ec2859d 100644 --- a/packages/ferrostar_flutter/test/models/user_location_test.dart +++ b/packages/ferrostar_flutter/test/models/user_location_test.dart @@ -1,5 +1,5 @@ -import 'package:flutter_test/flutter_test.dart'; import 'package:ferrostar_flutter/src/models/user_location.dart'; +import 'package:flutter_test/flutter_test.dart'; void main() { group('UserLocation', () { diff --git a/packages/ferrostar_flutter/test/models/visual_instruction_test.dart b/packages/ferrostar_flutter/test/models/visual_instruction_test.dart index e8cee2c..449c902 100644 --- a/packages/ferrostar_flutter/test/models/visual_instruction_test.dart +++ b/packages/ferrostar_flutter/test/models/visual_instruction_test.dart @@ -1,5 +1,5 @@ -import 'package:flutter_test/flutter_test.dart'; import 'package:ferrostar_flutter/src/models/visual_instruction.dart'; +import 'package:flutter_test/flutter_test.dart'; void main() { test('VisualInstruction with all fields round-trips', () {