Getting Started on Android

This section of the guide covers how to integrate Ferrostar into an Android app. We'll cover the "batteries included" approach, but flag areas for customization and overrides along the way.

Gradle setup

Add dependencies

build.gradle with explicit version strings

If you’re using the classic build.gradle with implementation strings using hard-coded versions, here’s how to set things up. Replace X.Y.Z with the latest release version.

dependencies {
    // Elided: androidx dependencies: ktx and compose standard deps

    // Ferrostar
    def ferrostarVersion = 'X.Y.Z'
    implementation "com.stadiamaps.ferrostar:core:${ferrostarVersion}"
    implementation "com.stadiamaps.ferrostar:maplibreui:${ferrostarVersion}"

    // Optional - if using Google Play Service's FusedLocation
    implementation "com.stadiamaps.ferrostar:google-play-services:${ferrostarVersion}"

    // okhttp3
    implementation platform("com.squareup.okhttp3:okhttp-bom:4.11.0")
    implementation 'com.squareup.okhttp3:okhttp'
}

libs.versions.toml “modern style”

If you’re using the newer libs.versions.toml approach, add the versions like so:

[versions]
ferrostar = "X.Y.X"
okhttp3 = "4.11.0"

[libraries]
ferrostar-core = { group = "com.stadiamaps.ferrostar", name = "core", version.ref = "ferrostar" }
ferrostar-maplibreui = { group = "com.stadiamaps.ferrostar", name = "maplibreui", version.ref = "ferrostar" }
okhttp3 = { group = "com.squareup.okhttp3", name = "okhttp", version.ref = "okhttp3" }

Then reference it in your build.gradle:

dependencies {
    // Elided: androidx dependencies: ktx and compose standard deps

    // Ferrostar
    implementation libs.ferrostar.core
    implementation libs.ferrostar.maplibreui

    // okhttp3
    implementation libs.okhttp3
}

Configure location services

Declaring permissions used

Your app will need access to the user’s location. First, you’ll need the requisite permissions in your Android manifest:

<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />

You’ll then need to request permission from the user to access their precise location.

Requesting location access

The “best” way to do this tends to change over time and varies with your app structure. If you’re using Jetpack Compose, you’ll want the rememberLauncherForActivityResult API. If you’re just using plain activities, the registerForActivityResult has what you need.

In either case, you’ll want to review Google’s documentation.

Ensuring updates when the app loses focus

Note that Ferrostar does not require “background” location access! This may be confusing if you’re new to mobile development. On Android, we can use something called a foreground service which lets us keep getting location updates even when the app isn’t front and center. This is such a detailed topic that it gets its own page! Learn about Foreground Service configuration here.

Location providers

You'll need to configure a provider to get location updates. We bundle a few implementations to get you started, or you can create your own. The broad steps are the same regardless of which provider you use: create an instance of the class, store it in an instance variable where it makes sense, and (if simulating a route) set the location manually or enter a simulated route.

Similar to the Android location APIs you may already know, you can add or remove listeners which will receive updates.

AndroidSystemLocationProvider

The AndroidSystemLocationProvider uses the location provider from the Android open-source project. This is not as good as the proprietary Google fused location client, but it is the most compatible option as it will run even on “un-Googled” phones and can be used in apps distributed on F-Droid.

Initializing this provider requires an Android Context, so you probably need to declare it as a lateinit var instance variable.

private lateinit var locationProvider: AndroidSystemLocationProvider

You can initialize it like so. In an Activity, the context is simply this. In other cases, get a context using an appropriate method.

locationProvider = AndroidSystemLocationProvider(context = this)

Google Play Fused Location Client

Alternatively, you can use the FusedLocationProvider if your app uses Google Play Services. This normally offers better device positioning than the default Android location provider on supported devices. To make use of it, you will need to include the optional implementation "com.stadiamaps.ferrostar:google-play-services:${ferrostarVersion}" in your Gradle dependencies block.

Just as with the AndroidSystemLocationProvider, you probably need to declare it as a lateinit var instance variable first, and then initialize later once the Context is available.

// Instance variable definition
private lateinit var locationProvider: FusedLocationProvider

// Later when the activity loads and a context is available
locationProvider = FusedLocationProvider(context = this)

SimulatedLocationProvider

The SimulatedLocationProvider allows for simulating location within Ferrostar without needing GPX files or complicated environment setup. This is great for testing and development without stepping outside.

First, instantiate the class. This will typically be saved as an instance variable.

private val locationProvider = SimulatedLocationProvider()

Later, most likely somewhere in your activity creation code or similar, set a location to your desired simulation start point.

locationProvider.lastLocation = initialSimulatedLocation

Once you have a route, you can simulate the replay of the route. This is technically optional (you can just set lastLocation yourself too), but playing back a route saves you the effort. You can set a warpFactor to play it back faster.

locationProvider.warpFactor = 2u
locationProvider.setSimulatedRoute(route)

You don’t need to do anything else after setting a simulated route; FerrostarCore will automatically add itself as a listener, which will trigger updates.

Configure an HTTP client

Before we configure the Ferrostar core, we need to set up an HTTP client. This is typically stored as an instance variable in one of your classes (ex: activity). We use the popular OkHttp library for this. Here we’ve set up a client with a global timeout of 15 seconds. Refer to the OkHttp documentation for further details on configuration.

private val httpClient = OkHttpClient.Builder()
    .callTimeout(Duration.ofSeconds(15))
    .build()

Configure the FerrostarCore instance

We now have all the pieces we need to set up a FerrostarCore instance! The FerrostarCore instance should live for at least the duration of a navigation session. Bringing it all together, a typical init looks something like this:

private val core =
      FerrostarCore(
          valhallaEndpointURL = URL("https://api.stadiamaps.com/route/v1?api_key=YOUR-API-KEY"),
          profile = "bicycle",
          httpClient = httpClient,
          locationProvider = locationProvider,
          foregroundServiceManager = foregroundServiceManager
      )

FerrostarCore exposes a number of convenience constructors for common cases, such as using a Valhalla Route Provider.

FerrostarCore automatically subscribes to location updates from the LocationProvider.

Set up voice guidance

Ferrostar is able to process spoken instructions generated from some routing engines. The com.stadiamaps.ferrostar.core.SpokenInstructionObserver interface specifies how to create your own observer. A reference implementation is provided in the AndroidTtsObserver class, which uses the text-to-speech engine built into Android. PRs welcome for other popular services (ex: Amazon Polly; note that some APIs also provide SSML instructions which work great with this!).

TODO documentation:

  • Android Manifest
  • Set the language
  • Additional config (you have full control; link to Android docs)

Getting a route

Getting a route is easy! All you need is the start location (from the location provider) and a list of at least one waypoint to visit.

val routes =
    core.getRoutes(
        userLocation,
        listOf(
            Waypoint(
                coordinate = GeographicCoordinate(37.807587, -122.428411),
                kind = WaypointKind.BREAK),
        ))

Note that this is a suspend function, so you’ll need to use it within a coroutine scope. You probably want something like launch(Dispatchers.IO) { .. } for most cases to ensure it’s running on the correct dispatcher. You may select a different dispatcher if you are doing offline route calculation.

Starting a navigation session

Once you have a route (ex: by grabbing the first one from the list or by presenting the user with a selection screen), you’re ready to start a navigation session!

When you start a navigation session, the core returns a view model which will automatically be updated with state updates.

You can save “rememberable” state somewhere near the top of your composable block like so:

var navigationViewModel by remember { mutableStateOf<NavigationViewModel?>(null) }

And then use it to store the result of your startNavigation invocation:

navigationViewModel =
    core.startNavigation(
        route = route,
        config =
        NavigationControllerConfig(
            StepAdvanceMode.RelativeLineStringDistance(
                minimumHorizontalAccuracy = 25U, automaticAdvanceDistance = 10U),
            RouteDeviationTracking.StaticThreshold(25U, 10.0)),
    )

Finally, If you’re simulating route progress (ex: in the emulator) with a SimulatedLocationProvider), set the route:

locationProvider.setSimulatedRoute(route)

Using the DynamicallyOrientingNavigationView

We’re finally ready to turn that view model into a beautiful navigation map! It’s really as simple as creating a DynamicallyOrientingNavigationView with the view model. Here’s an example:

 val viewModel = navigationViewModel
 if (viewModel != null) {
     // You can get a free Stadia Maps API key at https://client.stadiamaps.com.
     // See https://stadiamaps.github.io/ferrostar/vendors.html for additional vendors
     DynamicallyOrientingNavigationView(
         styleUrl =
         "https://tiles.stadiamaps.com/styles/outdoors.json?api_key=$stadiaApiKey",
         viewModel = viewModel) { uiState ->
         // You can add your own overlays here!
         // See https://github.com/Rallista/maplibre-compose-playground
     }
 } else {
     // Loading indicator
     Column(
         verticalArrangement = Arrangement.Center,
         horizontalAlignment = Alignment.CenterHorizontally) {
         Text(text = "Calculating route...")
         CircularProgressIndicator(modifier = Modifier.width(64.dp))
     }
 }

Tools for Improving a NavigationView

  • KeepScreenOnDisposableEffect is a simple disposable effect that automatically keeps the screen on and at consistent brightness while a user is on the scene using the effect. On dispose, the screen will return to default and auto lock and dim. See the demo app for an example of this being used alongside the DynamicallyOrientingNavigationView.

Demo app

We've put together a minimal demo app with an example integration.

Going deeper

This covers the basic “batteries included” configuration and pre-built UI. But there’s a lot of room for customization! Skip on over to the customization chapters that interest you.