Skip to main content

Headless navigation

Contents

Headless Navigation means integrating the CoPilot Native Navigation SDK without relying on the standard UI components (NavigationView). This approach allows developers to design a fully custom user interface while still leveraging the full power of the navigation engine for:

  • Route calculation and guidance
  • Turn-by-turn instructions
  • Voice guidance triggers
  • Real-time location tracking (road-snapped)
  • Off-route detection
  • Arrival detection

Key Difference from Standard Navigation

Feature Standard Navigation Headless Navigation
UI Components Pre-built NavigationView Custom (Compose, XML, etc.)
Map Display Integrated Optional/Custom
Voice Playback Built-in Custom TTS integration
Turn Cards Built-in Custom via observers
Flexibility Limited Full control

When to Use Headless Navigation

Use Headless Navigation When:

  • Custom UI Requirements: You need a completely custom navigation interface that doesn’t fit the standard NavigationView paradigm
  • Embedded Displays: Building for specialized hardware (fleet devices, embedded systems) with unique display requirements
  • Voice-Only Navigation: Building apps where visual navigation is secondary or optional
  • Background Navigation: Running navigation in a service without any UI
  • Integration with Existing Apps: Adding navigation to an existing app with established UI patterns
  • Gaming or AR Applications: Overlaying navigation data on non-traditional interfaces

Use Standard NavigationView When:

  • Rapid development is prioritized
  • Standard turn-by-turn UI is acceptable
  • You want built-in map camera following
  • You need route preview functionality out-of-the-box

Version History

  • SDK Version 1.1.0: Current documented version
  • Minimum SDK: 26
  • Target SDK: 33/34

SDK Dependencies

Add the following dependencies to your build.gradle:

dependencies {
  // Core Navigation SDK (required)
  implementation "com.trimblemaps.navigation:ui-components:1.1.0"

  // Maps SDK Services for geocoding/directions (required)
  implementation "com.trimblemaps.mapsdk:maps-sdk-services:1.1.0"

  // Optional: Route rendering plugin (if displaying routes on a map)
  implementation "com.trimblemaps.mapsdk:maps-android-plugin-route:1.1.1"
}

Required Permissions

<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<!-- For background navigation -->
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />

Core Components

TrimbleMapsNavigationProvider

The central provider for creating and managing the navigation instance.

import com.trimblemaps.navigation.core.TrimbleMapsNavigationProvider
import com.trimblemaps.navigation.core.TrimbleMapsNavigation

// Create navigation instance
val navigation: TrimbleMapsNavigation = TrimbleMapsNavigationProvider.create(options)

// Retrieve existing instance (if already created)
val navigation = TrimbleMapsNavigationProvider.retrieve()

// Destroy instance (call when done)
TrimbleMapsNavigationProvider.destroy()

TrimbleMapsTripProvider

Manages trip configuration including stops, routing profiles, and route options.

import com.trimblemaps.navigation.core.TrimbleMapsTripProvider
import com.trimblemaps.navigation.core.TrimbleMapsTrip

// Create a new trip
val trip: TrimbleMapsTrip = TrimbleMapsTripProvider.create()

// Retrieve existing trip
val trip = TrimbleMapsTripProvider.retrieve()

// Destroy trip (call when done)
TrimbleMapsTripProvider.destroy()

Configuration for the navigation engine.

import com.trimblemaps.navigation.base.options.NavigationOptions
import com.trimblemaps.navigation.core.internal.formatter.TrimbleMapsDistanceFormatter

val options = NavigationOptions.Builder(context)
  .isFromNavigationUi(false)  // CRITICAL: Set to false for headless mode
  .distanceFormatter(TrimbleMapsDistanceFormatter.Builder(context).build())
  .build()

Important Notes:

  • Setting .isFromNavigationUi(false) is required for headless navigation. This tells the SDK not to expect a NavigationView.
  • A distanceFormatter is required when using headless navigation. Without it, the navigation session will fail to start properly. Always provide a TrimbleMapsDistanceFormatter or your own DistanceFormatter in your NavigationOptions.

TripStop & TrimbleMapsLocation

Building blocks for defining route waypoints.

import com.trimblemaps.api.geocoding.v1.models.TrimbleMapsLocation
import com.trimblemaps.navigation.core.trip.model.TripStop

val stop = TripStop(
  TrimbleMapsLocation.builder()
    .coords(latitude, longitude)
    .placeName("Stop Name")
    .build()
)

Setup and Initialization

Step 1: Create Navigation Options

val options = NavigationOptions.Builder(context)
  .isFromNavigationUi(false)
  .distanceFormatter(TrimbleMapsDistanceFormatter.Builder(context).build())
  .build()

Note: Both isFromNavigationUi(false) and distanceFormatter() are required for headless navigation. The navigation session will not function correctly without a distance formatter.

Step 2: Create Navigation Instance

val navigation = TrimbleMapsNavigationProvider.create(options)

Step 3: Configure the Trip

val trip = TrimbleMapsTripProvider.create()

// Add origin
trip.addStop(
  TripStop(
    TrimbleMapsLocation.builder()
      .coords(40.361279, -74.600697)
      .placeName("Origin")
      .build()
  )
)

// Add destination
trip.addStop(
  TripStop(
    TrimbleMapsLocation.builder()
      .coords(40.349542, -74.660237)
      .placeName("Destination")
      .build()
  )
)

// Optional: Configure routing profile
trip.options.apply {
  routingProfile = yourRoutingProfile
  language = "en"
  alternatives = false
}

Step 4: Fetch Route and Start Navigation

val routeCallback = object : Callback<DirectionsResponse> {
  override fun onResponse(
    call: Call<DirectionsResponse>,
    response: Response<DirectionsResponse>
  ) {
    val routes = response.body()?.routes()
    if (!routes.isNullOrEmpty()) {
      navigation.setTrip(trip)
      navigation.startTripSession()
    }
  }

  override fun onFailure(call: Call<DirectionsResponse>, t: Throwable) {
    Log.e("Navigation", "Route fetch failed: ${t.message}")
  }
}

trip.fetchFullRoute(routeCallback)

Note: Use fetchFullRoute() when starting a new trip from the beginning. If a trip is already in progress (e.g., the user has passed some stops or you are resuming navigation), use fetchRemainingRoute() instead. This will calculate the route from the current position to the remaining stops, excluding any stops that have already been completed.


Observer Patterns

Headless navigation relies on observer callbacks to receive navigation updates. Register observers after creating the navigation instance.

Route Progress Observer

Provides real-time metrics about navigation progress.

navigation.registerRouteProgressObserver(object : RouteProgressObserver {
  override fun onRouteProgressChanged(routeProgress: RouteProgress) {
    // Distance remaining in meters
    val distanceRemaining = routeProgress.distanceRemaining

    // Duration remaining in seconds
    val durationRemaining = routeProgress.durationRemaining

    // Current step/leg progress
    val legProgress = routeProgress.currentLegProgress

    // Upcoming maneuver information
    val upcomingStep = legProgress?.upcomingStep
    if (upcomingStep != null) {
      val maneuver = upcomingStep.maneuver()
      val turnLocation = maneuver.location()  // Lat/Lng of turn
      val turnType = maneuver.type()          // e.g., "turn", "arrive"
      val turnModifier = maneuver.modifier()  // e.g., "left", "right"
    }
  }
})

Key Properties of RouteProgress:

Property Type Description
distanceRemaining Float Meters to destination
durationRemaining Double Seconds to destination
currentLegProgress RouteLegProgress? Progress on current leg
distanceTraveled Float Meters traveled so far
fractionTraveled Float 0.0 to 1.0 completion

Voice Instructions Observer

Triggers when a voice announcement should be played.

navigation.registerVoiceInstructionsObserver(object : VoiceInstructionsObserver {
  override fun onNewVoiceInstructions(voiceInstructions: VoiceInstructions) {
    val textToSpeak = voiceInstructions.announcement()

    // SSML version for enhanced TTS (if supported)
    val ssmlAnnouncement = voiceInstructions.ssmlAnnouncement()

    // Distance at which this instruction was triggered
    val distanceAlongGeometry = voiceInstructions.distanceAlongGeometry()

    // Play with Android TTS:
    // textToSpeech.speak(textToSpeak, TextToSpeech.QUEUE_FLUSH, null, "nav_id")
  }
})

Use Case: Integrate with android.speech.tts.TextToSpeech or third-party TTS engines.

Provides visual instruction data for turn cards/banners.

navigation.registerBannerInstructionsObserver(object : BannerInstructionsObserver {
  override fun onNewBannerInstructions(bannerInstructions: BannerInstructions) {
    val primary = bannerInstructions.primary()

    // Main instruction text
    val instructionText = primary.text()

    // Turn icon type (e.g., "turn", "merge", "roundabout")
    val iconType = primary.type()

    // Turn icon modifier (e.g., "left", "right", "sharp left")
    val iconModifier = primary.modifier()

    // Distance to this maneuver
    val distanceToManeuver = bannerInstructions.distanceAlongGeometry()

    // Secondary instruction (optional, e.g., "Then turn right")
    val secondary = bannerInstructions.secondary()
    if (secondary != null) {
      val secondaryText = secondary.text()
    }

    // Sub-instruction for lane guidance (optional)
    val sub = bannerInstructions.sub()
  }
})

Icon Type/Modifier Combinations:

Type Modifiers Description
turn left, right, sharp left, sharp right, slight left, slight right, straight, uturn Standard turns
merge left, right, straight Highway merges
off-ramp left, right Highway exits
fork left, right Road forks
roundabout left, right Roundabout navigation
arrive left, right, straight Arrival at destination

Off-Route Observer

Detects when the user has deviated from the planned route.

navigation.registerOffRouteObserver(object : OffRouteObserver {
  override fun onOffRouteStateChanged(offRoute: Boolean) {
    if (offRoute) {
      // User is off the planned route
      // Trigger reroute logic:
      // 1. Show "Rerouting..." in UI
      // 2. Optionally fetch a new route from current location
    }
  }
})

Location Observer

Provides both raw GPS and road-snapped (enhanced) location updates.

navigation.registerLocationObserver(object : LocationObserver {
  override fun onEnhancedLocationChanged(
    enhancedLocation: Location,
    keyPoints: List<Location>
  ) {
    // Road-snapped location - use this for:
    // - Map puck positioning
    // - ETA calculations
    // - Progress tracking
    val lat = enhancedLocation.latitude
    val lng = enhancedLocation.longitude
    val bearing = enhancedLocation.bearing
    val speed = enhancedLocation.speed
  }

  override fun onRawLocationChanged(rawLocation: Location) {
    // Unprocessed GPS location
    // Use for: debugging, accuracy indicators
  }
})

Enhanced vs. Raw Location:

Enhanced Location Raw Location
Snapped to road geometry Direct from GPS
Smoothed bearing May jump/jitter
Better for navigation display Better for accuracy metrics

Arrival Observer

Detects arrival at waypoints and final destination.

navigation.registerArrivalObserver(object : ArrivalObserver {
  override fun onFinalDestinationArrival(routeProgress: RouteProgress) {
    // User has arrived at the final destination
    // Show arrival screen, stop navigation, etc.
  }

  override fun onNextRouteLegStart(routeLegProgress: RouteLegProgress) {
    // User has arrived at an intermediate stop
    // and is starting the next leg
    val legIndex = routeLegProgress.legIndex
    // Update UI to show next stop info
  }
})

Simulated Navigation

For testing and development purposes, you can simulate navigation along a route without requiring real GPS movement. This is useful for:

  • Testing navigation logic without physical travel
  • Demonstrating navigation features
  • Automated testing
  • QA validation

Required Imports

import com.trimblemaps.navigation.core.replay.TrimbleMapsReplayer
import com.trimblemaps.navigation.core.replay.ReplayLocationEngine
import com.trimblemaps.navigation.core.replay.route.ReplayProgressObserver

Setting Up Simulation

Step 1: Create Replay Components

// Create the replayer and location engine
private val trimbleMapsReplayer = TrimbleMapsReplayer()
private val replayLocationEngine = ReplayLocationEngine(trimbleMapsReplayer)

Step 2: Configure NavigationOptions with ReplayLocationEngine

Replace the default location engine with the replay location engine:

val options = NavigationOptions.Builder(context)
  .isFromNavigationUi(false)
  .distanceFormatter(TrimbleMapsDistanceFormatter.Builder(context).build())
  .locationEngine(replayLocationEngine)  // Use replay engine instead of real GPS
  .build()

navigation = TrimbleMapsNavigationProvider.create(options)

Step 3: Register ReplayProgressObserver and Start Playback

After starting the trip session, register the ReplayProgressObserver and begin playback:

// After navigation.startTripSession()

// Register the replay observer to auto-feed route geometry as navigation progresses
navigation.registerRouteProgressObserver(ReplayProgressObserver(trimbleMapsReplayer))

// Set playback speed (1.0 = real-time, 2.0 = 2x speed, etc.)
val simulationSpeedMultiplier = 2.0
trimbleMapsReplayer.playbackSpeed(simulationSpeedMultiplier)

// Start the simulation
trimbleMapsReplayer.play()

Step 4: Cleanup on Destroy

Always finish the replayer when done:

fun onDestroy() {
  trimbleMapsReplayer.finish()  // Stop simulation
  navigation.stopTripSession()
  navigation.onDestroy()
}

Complete Simulation Example

class SimulatedNavigationManager(
  private val context: Context,
  private val onStatusUpdate: (String) -> Unit = {}
) {
  private val navigation: TrimbleMapsNavigation

  // Simulation components
  private val trimbleMapsReplayer = TrimbleMapsReplayer()
  private val replayLocationEngine = ReplayLocationEngine(trimbleMapsReplayer)

  init {
    val options = NavigationOptions.Builder(context)
      .isFromNavigationUi(false)
      .distanceFormatter(TrimbleMapsDistanceFormatter.Builder(context).build())
      .locationEngine(replayLocationEngine)
      .build()

    navigation = TrimbleMapsNavigationProvider.create(options)
    setupObservers()
  }

  fun startSimulatedTrip(trip: TrimbleMapsTrip, speedMultiplier: Double = 2.0) {
    trip.fetchFullRoute(object : Callback<DirectionsResponse> {
      override fun onResponse(
        call: Call<DirectionsResponse>,
        response: Response<DirectionsResponse>
      ) {
        val routes = response.body()?.routes()
        if (routes.isNullOrEmpty()) {
          onStatusUpdate("Error: No routes found")
          return
        }

        navigation.setTrip(trip)
        navigation.startTripSession()

        // Setup simulation
        navigation.registerRouteProgressObserver(
          ReplayProgressObserver(trimbleMapsReplayer)
        )
        trimbleMapsReplayer.playbackSpeed(speedMultiplier)
        trimbleMapsReplayer.play()

        onStatusUpdate("Simulating at ${speedMultiplier}x speed")
      }

      override fun onFailure(call: Call<DirectionsResponse>, t: Throwable) {
        onStatusUpdate("Error: ${t.message}")
      }
    })
  }

  fun onDestroy() {
    trimbleMapsReplayer.finish()
    navigation.stopTripSession()
    navigation.onDestroy()
  }

  private fun setupObservers() {
    // Register your other observers here (RouteProgress, Voice, Banner, etc.)
  }
}

Simulation Speed Reference

Speed Multiplier Description
0.5 Half speed (slow motion)
1.0 Real-time speed
2.0 2x speed (recommended for testing)
5.0 5x speed (quick testing)
10.0 10x speed (rapid testing)

Important Notes

  • The ReplayProgressObserver automatically feeds simulated locations based on route progress
  • Simulation uses the route geometry to generate realistic location updates
  • All observers (RouteProgress, Voice, Banner, etc.) will fire as if real navigation is occurring
  • Off-route detection will not trigger during simulation since locations follow the route exactly

Complete Code Example

A complete, production-ready skeleton for headless navigation:

package com.example.navigation

import android.annotation.SuppressLint
import android.content.Context
import android.location.Location
import android.util.Log
import com.trimblemaps.api.directions.v1.models.BannerInstructions
import com.trimblemaps.api.directions.v1.models.DirectionsResponse
import com.trimblemaps.api.directions.v1.models.VoiceInstructions
import com.trimblemaps.navigation.base.options.NavigationOptions
import com.trimblemaps.navigation.base.trip.model.RouteLegProgress
import com.trimblemaps.navigation.base.trip.model.RouteProgress
import com.trimblemaps.navigation.core.TrimbleMapsNavigation
import com.trimblemaps.navigation.core.TrimbleMapsNavigationProvider
import com.trimblemaps.navigation.core.TrimbleMapsTrip
import com.trimblemaps.navigation.core.arrival.ArrivalObserver
import com.trimblemaps.navigation.core.trip.session.*
import com.trimblemaps.navigation.core.internal.formatter.TrimbleMapsDistanceFormatter
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response

/**
 * Headless Navigation Manager
 *
 * Handles all navigation SDK interactions without UI components.
 * Provides callbacks for integrating with custom UI frameworks.
 */
class NavigationManager(
  private val context: Context,
  private val callbacks: NavigationCallbacks
) {
  private val navigation: TrimbleMapsNavigation

  interface NavigationCallbacks {
    fun onNavigationReady()
    fun onNavigationError(error: String)
    fun onRouteProgressUpdate(
      distanceRemaining: Float,
      durationRemaining: Double,
      fractionComplete: Float
    )
    fun onVoiceInstruction(announcement: String, ssml: String?)
    fun onBannerInstruction(
      text: String,
      type: String?,
      modifier: String?,
      distanceToManeuver: Float?
    )
    fun onLocationUpdate(location: Location)
    fun onOffRoute(isOffRoute: Boolean)
    fun onArrival(isFinalDestination: Boolean, legIndex: Int)
  }

  init {
    val options = NavigationOptions.Builder(context)
      .isFromNavigationUi(false)
      .distanceFormatter(TrimbleMapsDistanceFormatter.Builder(context).build())
      .build()

    navigation = TrimbleMapsNavigationProvider.create(options)
    setupObservers()
  }

  @SuppressLint("MissingPermission")
  fun startNavigation(trip: TrimbleMapsTrip) {
    trip.fetchFullRoute(object : Callback<DirectionsResponse> {
      override fun onResponse(
        call: Call<DirectionsResponse>,
        response: Response<DirectionsResponse>
      ) {
        val routes = response.body()?.routes()
        if (routes.isNullOrEmpty()) {
          callbacks.onNavigationError("No routes found")
          return
        }

        navigation.setTrip(trip)
        navigation.startTripSession()
        callbacks.onNavigationReady()
      }

      override fun onFailure(call: Call<DirectionsResponse>, t: Throwable) {
        callbacks.onNavigationError(t.message ?: "Unknown error")
      }
    })
  }

  fun stopNavigation() {
    navigation.stopTripSession()
  }

  fun destroy() {
    navigation.stopTripSession()
    navigation.onDestroy()
  }

  private fun setupObservers() {
    // Route Progress
    navigation.registerRouteProgressObserver(object : RouteProgressObserver {
      override fun onRouteProgressChanged(progress: RouteProgress) {
        callbacks.onRouteProgressUpdate(
          progress.distanceRemaining,
          progress.durationRemaining,
          progress.fractionTraveled
        )
      }
    })

    // Voice Instructions
    navigation.registerVoiceInstructionsObserver(object : VoiceInstructionsObserver {
      override fun onNewVoiceInstructions(instructions: VoiceInstructions) {
        callbacks.onVoiceInstruction(
          instructions.announcement() ?: "",
          instructions.ssmlAnnouncement()
        )
      }
    })

    // Banner Instructions
    navigation.registerBannerInstructionsObserver(object : BannerInstructionsObserver {
      override fun onNewBannerInstructions(instructions: BannerInstructions) {
        val primary = instructions.primary()
        callbacks.onBannerInstruction(
          primary.text(),
          primary.type(),
          primary.modifier(),
          instructions.distanceAlongGeometry()?.toFloat()
        )
      }
    })

    // Location Updates
    navigation.registerLocationObserver(object : LocationObserver {
      override fun onEnhancedLocationChanged(
        location: Location,
        keyPoints: List<Location>
      ) {
        callbacks.onLocationUpdate(location)
      }

      override fun onRawLocationChanged(rawLocation: Location) {
        // Optionally expose raw location
      }
    })

    // Off-Route Detection
    navigation.registerOffRouteObserver(object : OffRouteObserver {
      override fun onOffRouteStateChanged(offRoute: Boolean) {
        callbacks.onOffRoute(offRoute)
      }
    })

    // Arrival Detection
    navigation.registerArrivalObserver(object : ArrivalObserver {
      override fun onFinalDestinationArrival(progress: RouteProgress) {
        callbacks.onArrival(true, -1)
      }

      override fun onNextRouteLegStart(legProgress: RouteLegProgress) {
        callbacks.onArrival(false, legProgress.legIndex)
      }
    })
  }
}

Usage Example (Jetpack Compose)

class HeadlessNavActivity : ComponentActivity(), NavigationManager.NavigationCallbacks {
  private lateinit var navManager: NavigationManager

  // Compose state
  private var distance by mutableStateOf("--")
  private var instruction by mutableStateOf("--")

  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)

    navManager = NavigationManager(this, this)

    // Build trip
    val trip = TrimbleMapsTripProvider.create()
    trip.addStop(/* origin */)
    trip.addStop(/* destination */)

    navManager.startNavigation(trip)

    setContent {
      // Your custom Compose UI using distance, instruction, etc.
    }
  }

  override fun onRouteProgressUpdate(
    distanceRemaining: Float,
    durationRemaining: Double,
    fractionComplete: Float
  ) {
    distance = "%.1f km".format(distanceRemaining / 1000)
  }

  override fun onBannerInstruction(
    text: String,
    type: String?,
    modifier: String?,
    distanceToManeuver: Float?
  ) {
    instruction = text
  }

  // Implement other callbacks...

  override fun onDestroy() {
    super.onDestroy()
    navManager.destroy()
  }
}

API Reference

TrimbleMapsNavigation Methods

Method Description
setTrip(trip) Set the trip to navigate
startTripSession() Begin navigation session
stopTripSession() End navigation session
onDestroy() Cleanup resources
registerRouteProgressObserver(observer) Register progress observer
registerVoiceInstructionsObserver(observer) Register voice observer
registerBannerInstructionsObserver(observer) Register banner observer
registerLocationObserver(observer) Register location observer
registerOffRouteObserver(observer) Register off-route observer
registerArrivalObserver(observer) Register arrival observer

TrimbleMapsTrip Methods

Method Description
addStop(tripStop) Add a stop to the trip
removeStop(index) Remove a stop by index
fetchFullRoute(callback) Fetch route from API
options Access trip options (profile, language, etc.)
getNavigationRoutes() Get calculated routes

Limitations

Current Limitations

  1. No Built-in Map Camera Control
    • You must implement your own camera following logic if displaying a map
    • Use LocationObserver enhanced location for camera positioning
  2. No Built-in Route Line Rendering
    • Must use maps-android-plugin-route separately
    • Route geometry available from DirectionsResponse
  3. No Automatic Rerouting
    • OffRouteObserver notifies when off-route
    • You must implement reroute logic (fetch new route, update trip)
  4. No Built-in Voice Playback
    • Only provides text/SSML for announcements
    • Must integrate with android.speech.tts.TextToSpeech or similar
  5. No UI for Speed Limits or Warnings
    • Must implement custom UI for speed data
    • Speed available via RouteProgress
  6. Location Permission Handling
    • Must implement location permission requests yourself
    • Navigation will fail without proper permissions

Memory Considerations

  • Always call navigation.onDestroy() when done
  • Always call TrimbleMapsNavigationProvider.destroy() before creating new instances
  • Register observers once; don’t re-register on configuration changes

Best Practices

Lifecycle Management

override fun onDestroy() {
  super.onDestroy()
  navigation.stopTripSession()
  navigation.onDestroy()
}

Error Handling

trip.fetchFullRoute(object : Callback<DirectionsResponse> {
  override fun onResponse(...) {
    val body = response.body()
    if (body == null) {
      handleError("Null response")
      return
    }
    val routes = body.routes()
    if (routes.isNullOrEmpty()) {
      handleError("No routes found")
      return
    }
    // Continue with navigation
  }

  override fun onFailure(call, t) {
    handleError(t.message ?: "Network error")
  }
})

Threading

  • All observers are called on the main thread
  • Safe to update UI directly in observer callbacks
  • For heavy processing, dispatch to background threads

State Preservation

  • Handle configuration changes (screen rotation)
  • Consider using ViewModel to survive configuration changes
  • Store navigation state for process death recovery

Testing

// Use simulation mode for testing without real GPS
// Note: Simulation must be configured before starting navigation

Troubleshooting

  • Ensure location permissions are granted
  • Verify isFromNavigationUi(false) is set in NavigationOptions
  • Check that route fetch succeeded before calling startTripSession()

Observers Not Firing

  • Verify observers are registered before startTripSession()
  • Check that navigation session actually started (no route fetch errors)
  • Ensure you’re not unregistering observers prematurely

Memory Leaks

  • Always call onDestroy() on the navigation instance
  • Don’t hold references to Context in observer callbacks
  • Consider using WeakReference for long-lived callbacks
Last updated January 28, 2026.