feat(plugin): ferrostar_flutter v0.1.0 — iOS Flutter plugin for turn-by-turn navigation - #29
Merged
Conversation
Flutter + MapLibre + Riverpod + Dio approach for iOS-first (Android later) mobile port of Ortschaft. Basic turn-by-turn navigation in v1 with extensibility seams for full navigation later. Uses existing backend as-is with one small extension (pass through GraphHopper instructions).
Switch navigation engine from custom-built to Ferrostar (Stadia Maps Rust-core navigation SDK with Dart bindings). Ferrostar handles route matching, instruction tracking, ETA, and off-route detection out of the box and supports GraphHopper natively — saves 4-8 weeks of custom work. Also add painting (brush tool) to the extensibility seams section per feedback — mobile overlay rendering is read-only for v1 but the full paint tool is planned, not architecturally precluded.
Reflects the new distance_influence parameter added to POST /api/route in #16 — mobile client needs to accept it (we default to not sending, letting backend use its default of 70).
The original spec scoped navigation conservatively because we planned to build it ourselves. With Ferrostar we get voice guidance, auto-rerouting, and correctly-timed banner instructions for free — include them in v1 instead of deferring. Backend change revised from 'pass through instructions' to new POST /api/navigate endpoint that proxies GraphHopper's /navigate/directions. This returns Mapbox Directions v5-compatible JSON which Ferrostar's built-in OSRM adapter consumes verbatim (with voice_instructions and banner_instructions arrays). No client-side instruction synthesis needed in the happy path. Flagged the GraphHopper /navigate endpoint availability as a prerequisite to verify during implementation, with a custom-adapter fallback.
Ferrostar has no Dart bindings as of 2026 — the plan to use it from Flutter requires building our own plugin. Rather than vendor that work inside the app codebase, structure the plugin as an independent artifact at packages/ferrostar_flutter/ with its own pubspec, tests, and example app. Scope plugin v0.1 to only what the app needs (opaque handles for Route/TripState, derived data via method channel, synchronous Navigator API, 3 event streams). Spec now decomposes into three implementation plans: the plugin (Plan A, largest), the backend /api/navigate endpoint (Plan B, smallest), and the mobile app that consumes both (Plan C).
Co-authored-by: Codex
- Replace NavigationController with Navigator (createNavigator factory) - Store config in ControllerRegistry.Entry for replaceRoute reuse - Fix TripState.navigating positional pattern matching (11 named fields) - Fix ManeuverType/ManeuverModifier to custom string mapping (no rawValue) - Fix WaypointKind: .via / .break (not .viaPoint / .breakPoint) - Fix NavigationControllerConfig: add arrivalStepAdvanceCondition via stepAdvanceManual() factory; use stepAdvanceDistanceToEndOfStep() - Add conditional Flutter/FlutterFramework import to all bridge files
- ControllerBridge: extract routes[0] from full OSRM response before passing to createRouteFromOsrmRoute; use createRouteFromOsrm with waypointData for replaceRoute so snapped waypoints are preserved - method_channel_platform: deep-normalize platform-channel payloads (Map<Object?,Object?> -> Map<String,dynamic>) before fromJson to fix _TypeError on nested event maps - StepRef.index: make nullable (Ferrostar provides remaining steps only, no global step index is available from TripState)
- test-flutter-plugin: flutter test on ubuntu-latest (unit tests, runs on every PR/push) - test-flutter-plugin-ios: integration test on macos-latest with iPhone 17 simulator; gated to push-to-main or PRs labelled test-ios to avoid burning macOS minutes on unrelated changes
Separate workflow triggers only on changes under packages/ferrostar_flutter/, so macOS minutes are not spent on unrelated backend/frontend PRs.
flutter config --enable-swift-package-manager must be set globally; without it Flutter falls back to CocoaPods which rejects the SPM-only plugin.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
ferrostar_flutterFlutter plugin package atpackages/ferrostar_flutter/createController,updateLocation,replaceRoute,dispose, plus streams forNavigationState,SpokenInstruction,RouteDeviationfreezed+json_serializablewith explicittoJsonWhat changed
iOS Swift bridge (
ios/ferrostar_flutter/Sources/ferrostar_flutter/)FerrostarFlutterPluginregisters the method channel and stores the binary messengerControllerBridgehandles all method calls; usescreateNavigator()/Navigator(notNavigationController) — the correct Ferrostar 0.49 typeControllerRegistrystores opaqueNavigator+NavigationControllerConfigentries by UUID handle;Entryis afinal classfor direct mutation of event sinksSerializationmaps Ferrostar types ↔ Dart-friendly dicts; handles 11-fieldTripState.navigatingpositional pattern, customManeuverType/ManeuverModifierstring maps (no.rawValue)StreamEmitterswires oneFlutterEventChannelper (controller, kind) tupleOSRM parsing fix (last commit)
extractRouteDatapullsroutes[0]from the full OSRM response before passing tocreateRouteFromOsrmRoute— previously the full response was passed, causing a parse errorreplaceRoutenow usescreateRouteFromOsrm(routeData:waypointData:)to preserve snapped waypointsDart event deserialization fix (last commit)
_deepNormalize()inmethod_channel_platform.dartrecursively convertsMap<Object?,Object?>→Map<String,dynamic>at all nesting levels; the previous shallowMap.from()caused_TypeErroron nested payloads from iOS event channelsStepRef.indexmade nullable — Ferrostar only exposes remaining steps inTripState.navigating, no global step index is availableTest plan
flutter test— 21/21 unit tests passflutter build apk --debug— Android seam compilesflutter build ios --no-codesign --simulator— iOS build succeedsflutter test integration_test/plugin_integration_test.dart -d "iPhone 17"— 2/2 integration tests pass