Skip to content

feat(plugin): ferrostar_flutter v0.1.0 — iOS Flutter plugin for turn-by-turn navigation - #29

Merged
cafca merged 29 commits into
mainfrom
claude/naughty-borg-618d9d
Apr 17, 2026
Merged

feat(plugin): ferrostar_flutter v0.1.0 — iOS Flutter plugin for turn-by-turn navigation#29
cafca merged 29 commits into
mainfrom
claude/naughty-borg-618d9d

Conversation

@cafca

@cafca cafca commented Apr 17, 2026

Copy link
Copy Markdown
Owner

Summary

  • Scaffolds the ferrostar_flutter Flutter plugin package at packages/ferrostar_flutter/
  • Implements full iOS bridge to Ferrostar 0.49.x via Swift Package Manager — method channel + 3 event channels (state, spoken instructions, route deviation)
  • Android: compile-only seam stubs (full implementation deferred)
  • Dart API: createController, updateLocation, replaceRoute, dispose, plus streams for NavigationState, SpokenInstruction, RouteDeviation
  • All models use freezed + json_serializable with explicit toJson
  • Integration test passes on iPhone 17 simulator (create → updateLocation → receive navigating state)

What changed

iOS Swift bridge (ios/ferrostar_flutter/Sources/ferrostar_flutter/)

  • FerrostarFlutterPlugin registers the method channel and stores the binary messenger
  • ControllerBridge handles all method calls; uses createNavigator() / Navigator (not NavigationController) — the correct Ferrostar 0.49 type
  • ControllerRegistry stores opaque Navigator + NavigationControllerConfig entries by UUID handle; Entry is a final class for direct mutation of event sinks
  • Serialization maps Ferrostar types ↔ Dart-friendly dicts; handles 11-field TripState.navigating positional pattern, custom ManeuverType/ManeuverModifier string maps (no .rawValue)
  • StreamEmitters wires one FlutterEventChannel per (controller, kind) tuple

OSRM parsing fix (last commit)

  • extractRouteData pulls routes[0] from the full OSRM response before passing to createRouteFromOsrmRoute — previously the full response was passed, causing a parse error
  • replaceRoute now uses createRouteFromOsrm(routeData:waypointData:) to preserve snapped waypoints

Dart event deserialization fix (last commit)

  • _deepNormalize() in method_channel_platform.dart recursively converts Map<Object?,Object?>Map<String,dynamic> at all nesting levels; the previous shallow Map.from() caused _TypeError on nested payloads from iOS event channels
  • StepRef.index made nullable — Ferrostar only exposes remaining steps in TripState.navigating, no global step index is available

Test plan

  • flutter test — 21/21 unit tests pass
  • flutter build apk --debug — Android seam compiles
  • flutter build ios --no-codesign --simulator — iOS build succeeds
  • flutter test integration_test/plugin_integration_test.dart -d "iPhone 17" — 2/2 integration tests pass

cafca added 29 commits April 16, 2026 10:39
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).
- 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.
@cafca
cafca merged commit 63f7084 into main Apr 17, 2026
7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant