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
Let’s get started with Gradle setup.
Replace X.Y.Z
with the latest release version.
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.
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.
Google Play Fused Location Client
If your app uses Google Play Services,
you can use the FusedLocationProvider
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)
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 will run on any Android phone,
including ones without Google Play Services.
It is also compatible with stores like F-Droid,
which require all apps use open-source software.
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)
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,
navigationControllerConfig =
NavigationControllerConfig(
StepAdvanceMode.RelativeLineStringDistance(
minimumHorizontalAccuracy = 25U, automaticAdvanceDistance = 10U),
RouteDeviationTracking.StaticThreshold(15U, 50.0),
CourseFiltering.SNAP_TO_ROUTE),
)
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
If your routes include spoken instructions,
Ferrostar can trigger the speech synthesis at the right time.
Ferrostar includes the AndroidTtsObserver
class,
which uses the text-to-speech engine built into Android.
You can also use your own implementation,
such as a local AI model or cloud service like Amazon Polly.
The com.stadiamaps.ferrostar.core.SpokenInstructionObserver
interface
specifies the required API.
PRs welcome to add other publicly accessible speech API implementations.
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:
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 put this together into a beautiful navigation map!
FerrostarCore
exposes a state flow,
which you can incorporate into your own view model,
which must implement the NavigationViewModel
interface.
See the DemoNavigationViewModel
for an example of what a view model might look like.
Here’s an example:
// 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 the DemoNavigationScene or https://github.com/Rallista/maplibre-compose-playground
// for some examples.
}
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 theDynamicallyOrientingNavigationView
.
Demo app
We've put together a minimal demo app to show how to integrate Ferrostar into your Android app.
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.