androidengineers.Book a session

Designing a Ride-Sharing App

Routing & ETAs

article20 minHard

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

ApproachWhen to use
Google Directions API (server)Most apps — authoritative routing, traffic-aware
Routes API (newer, more features)Fleet routing, waypoints, vehicle routing
Mapbox Navigation SDKWhen you need offline routing or custom styling
Client-side simple pathVisual-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

ConceptRule
Routes APIUse over Directions API for new apps; traffic-aware, more features
Encoded polylineCompact wire format; decode with PolyUtil.decode()
geodesic = trueRequired for routes spanning long distances; curves along earth's surface
Dynamic ETARecalculate every 30s during active navigation; sample to avoid hammering API
zoomToRouteFit the camera to the route bounds on route load
Offline tilesPredownload region tiles when connectivity is expected to drop

YOUR LEARNING JOURNEY

0 of 177 available lessons completed

Progress saved in this browser. No account needed.
Routing & ETAs | Android System Design | Android Engineers