From bb7ec17fadcd67f24f1f9589ced015ff2914a9ea Mon Sep 17 00:00:00 2001 From: Matthew Podwysocki Date: Thu, 1 Oct 2026 11:15:02 -0400 Subject: [PATCH] Cover Android custom-UI APIs in navigation AGENTS.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit AGENTS.md ships standalone — README.md tells users to curl it into a project root — and it was silent on four Android Navigation SDK areas that references/ covers in detail. Silence in a condensed file is not neutral: asked about them, the model filled the gaps by inventing APIs. It named `upcomingRouteAlerts` instead of `RouteProgress.upcomingRoadObjects`, inverted `addUpcomingManeuverArrow` and `updateUpcomingManeuverArrow`, and said calling `MapboxAudioGuidance.create()` directly is wrong when it is the correct call if you manage the instance lifecycle yourself. Added compact coverage of each, drawn from references/android-navigation-sdk.md and references/android-performance-antipatterns.md: - Route line via MapboxRouteLineApi/View, with getAlternativeMetadataFor and the cancel() teardown neither object does on its own - Maneuver arrows as a separate API/View pair, recomputed per progress update rather than drawn once - MapboxAudioGuidance.getRegisteredInstance(), which self-registers with the MapboxNavigationApp lifecycle, and when create() is right instead - RouteProgress.upcomingRoadObjects, already filtered and ordered, versus re-filtering NavigationRoute.upcomingRoadObjects by position - Observer registration through onAttached/onDetached, since an inline lambda with no stored reference can never be unregistered - Why reading route data under nativeRouteObject(true) is expensive On the agents surface at n=5, the four affected evals moved from 0/9, 5/12, 6/12 and 8/12; the skill went 82% to 97%. --- skills/mapbox-navigation-patterns/AGENTS.md | 60 +++++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/skills/mapbox-navigation-patterns/AGENTS.md b/skills/mapbox-navigation-patterns/AGENTS.md index eb5fea7..2bd14ea 100644 --- a/skills/mapbox-navigation-patterns/AGENTS.md +++ b/skills/mapbox-navigation-patterns/AGENTS.md @@ -317,6 +317,66 @@ override fun onStop() { } ``` +### Custom UI: Route Line, Arrows, Voice, Road Objects + +These are distinct API/View pairs — the maneuver arrow is not part of the route line +component, and neither is the voice guidance. + +```kotlin +// Route line — MapboxRouteLineApi + MapboxRouteLineView. +// Pass alternatives metadata alongside the routes, and release both on teardown: +// neither stops work on its own when the screen goes away. +private val routesObserver = RoutesObserver { result -> + val metadata = mapboxNavigation.getAlternativeMetadataFor(result.navigationRoutes) + routeLineApi.setNavigationRoutes(result.navigationRoutes, metadata) { value -> + routeLineView.renderRouteDrawData(mapView.mapboxMap.style!!, value) + } +} + +override fun onDestroy() { + super.onDestroy() + routeLineApi.cancel() + routeLineView.cancel() +} + +// Maneuver arrow — MapboxRouteArrowApi + MapboxRouteArrowView, a separate pair from +// the route line. Recompute on every progress update; it is not a one-time draw. +private val routeProgressObserver = RouteProgressObserver { routeProgress -> + val arrow = routeArrowApi.addUpcomingManeuverArrow(routeProgress) + routeArrowView.renderManeuverUpdate(mapView.mapboxMap.style!!, arrow) +} + +// Voice guidance — prefer the registered instance. It self-registers with the +// MapboxNavigationApp lifecycle and handles prefetching and mute state for you. +val audioGuidance = MapboxAudioGuidance.getRegisteredInstance() +// MapboxAudioGuidance.create() is the correct call only when you want a standalone +// instance whose lifecycle you manage yourself. Never use both: two independently +// constructed instances each drive their own player and race to speak. + +// Road objects ahead — read the precomputed list rather than filtering +// RouteLeg.incidents()/closures() yourself. RouteProgress.upcomingRoadObjects is +// already filtered to what is ahead and ordered by distance, with distanceToStart +// computed. NavigationRoute.upcomingRoadObjects is the unfiltered whole-route +// equivalent, so re-filtering that one by position is the same antipattern. +val ahead = routeProgress.upcomingRoadObjects +``` + +Register and unregister these observers in the `onAttached`/`onDetached` callbacks of the +`onResumedObserver` passed to `requireMapboxNavigation`. An observer registered as an +inline lambda with no stored reference can never be unregistered, which leaks the screen. + +`MapboxNavigation` is invalid after `onDestroy()`/`MapboxNavigationProvider.destroy()` — +holding it in a singleton and touching it afterwards is a crash, not just a leak. + +### Reading Route Data Cheaply + +With `nativeRouteObject(true)`, every `legs()`, `steps()`, or `annotation()` access +re-reads and re-converts data from native memory, and the result is not cached between +calls. Read the precomputed `RouteProgress` fields instead of walking the route: +`distanceRemaining`, `durationRemaining`, `fractionTraveled`, `upcomingRoadObjects`, +`bannerInstructions`. This matters most inside `RouteProgressObserver`, which fires up to +10 times per second on the main thread, where the work blocks frame rendering. + ## Routing Profiles | Profile | Use Case |