Routing and ETAs are core to ride-hailing, food delivery, and navigation apps. Understanding the trade-offs between client-side and server-side routing, polyline rendering, and ETA estimation helps you build accurate, performant map experiences.
Routes: Client vs Server
| Approach | When to use |
|---|---|
| Google Directions API (server) | Most apps — authoritative routing, traffic-aware |
| Routes API (newer, more features) | Fleet routing, waypoints, vehicle routing |
| Mapbox Navigation SDK | When you need offline routing or custom styling |
| Client-side simple path | Visual-only connecting lines, not real roads |
Fetching a Route with Routes API
// Use the Routes API for optimal results (replaces Directions API)
class RoutingRepository(private val mapsApi: GoogleMapsApi) {
suspend fun getRoute(
origin: LatLng,
destination: LatLng
): RouteResult = withContext(Dispatchers.IO) {
val request = ComputeRoutesRequest(
origin = Waypoint.fromLatLng(origin),
destination = Waypoint.fromLatLng(destination),
travelMode = RouteTravelMode.DRIVE,
routingPreference = RoutingPreference.TRAFFIC_AWARE,
computeAlternativeRoutes = true
)
val response = mapsApi.computeRoutes(request)
RouteResult(
polyline = response.routes.first().polyline.encodedPolyline,
durationSeconds = response.routes.first().duration.seconds,
distanceMeters = response.routes.first().distanceMeters
)
}
}
Decoding and Drawing Polylines
// Decode encoded polyline to LatLng list
fun decodePolyline(encoded: String): List<LatLng> {
return PolyUtil.decode(encoded) // from maps-utils
}
// Draw on Google Maps
fun drawRoute(map: GoogleMap, encodedPolyline: String): Polyline {
val points = decodePolyline(encodedPolyline)
return map.addPolyline(
PolylineOptions()
.addAll(points)
.color(Color.parseColor("#4285F4"))
.width(12f)
.geodesic(true) // renders along earth's curvature
.startCap(RoundCap())
.endCap(RoundCap())
)
}
// Animate camera to fit the route
fun zoomToRoute(map: GoogleMap, points: List<LatLng>) {
val bounds = LatLngBounds.builder()
.also { builder -> points.forEach { builder.include(it) } }
.build()
map.animateCamera(CameraUpdateFactory.newLatLngBounds(bounds, 120))
}
ETA Estimation
// Simple ETA from route duration
fun computeEta(durationSeconds: Long): String {
val arrivalTime = System.currentTimeMillis() + durationSeconds * 1000
val formatter = SimpleDateFormat("h:mm a", Locale.getDefault())
return formatter.format(Date(arrivalTime))
}
// Dynamic ETA: re-request route periodically as conditions change
class DynamicEtaManager(
private val routingRepository: RoutingRepository,
private val onEtaUpdate: (String) -> Unit
) {
private var updateJob: Job? = null
fun start(destination: LatLng, locationFlow: Flow<Location>) {
updateJob = coroutineScope.launch {
locationFlow
.sample(30_000) // recalculate every 30 seconds
.collect { currentLocation ->
val origin = LatLng(currentLocation.latitude, currentLocation.longitude)
val result = runCatching { routingRepository.getRoute(origin, destination) }
.getOrNull() ?: return@collect
onEtaUpdate(computeEta(result.durationSeconds))
}
}
}
fun stop() { updateJob?.cancel() }
}
Turn-by-Turn Navigation Prompt
data class TurnByTurnStep(
val instruction: String, // "Turn left on Main St"
val distanceMeters: Int, // distance until this maneuver
val maneuver: Maneuver // TURN_LEFT, TURN_RIGHT, STRAIGHT, etc.
)
class NavigationPromptManager(private val ttsEngine: TextToSpeech) {
fun announceStep(step: TurnByTurnStep, metersToManeuver: Int) {
when {
metersToManeuver < 50 -> ttsEngine.speak(step.instruction, QUEUE_FLUSH, null, null)
metersToManeuver < 200 -> ttsEngine.speak("In 200 meters, ${step.instruction}", QUEUE_FLUSH, null, null)
else -> { /* no announcement yet */ }
}
}
}
Offline Tile Caching
For areas with poor connectivity:
// Download tiles for an area (Google Maps SDK supports offline regions via Maps SDK)
val southwest = LatLng(latitude - 0.1, longitude - 0.1)
val northeast = LatLng(latitude + 0.1, longitude + 0.1)
OfflineTileProvider.downloadRegion(
bounds = LatLngBounds(southwest, northeast),
minZoom = 10,
maxZoom = 16
)
Key Takeaways
| Concept | Rule |
|---|---|
| Routes API | Use over Directions API for new apps; traffic-aware, more features |
| Encoded polyline | Compact wire format; decode with PolyUtil.decode() |
geodesic = true | Required for routes spanning long distances; curves along earth's surface |
| Dynamic ETA | Recalculate every 30s during active navigation; sample to avoid hammering API |
zoomToRoute | Fit the camera to the route bounds on route load |
| Offline tiles | Predownload region tiles when connectivity is expected to drop |