androidengineers.Book a session

Designing a Ride-Sharing App

Exercise: Live Location Demo

exercise55 minHard

Build a real-time location sharing screen: track your own location, share it over WebSocket, and display another user's position on a map that updates live.

Goal

  • Track current user's location (foreground service)
  • Emit location updates over WebSocket
  • Receive partner's location updates and move their map marker smoothly
  • Show ETA from current user to partner

Step 1: Location Tracking

class LiveLocationRepository(
    private val context: Context,
    private val socket: LocationWebSocket
) {
    private val fusedLocationClient = LocationServices.getFusedLocationProviderClient(context)

    fun startSharing(): Flow<Location> = callbackFlow {
        val request = LocationRequest.Builder(Priority.PRIORITY_HIGH_ACCURACY, 3_000L)
            .setMinUpdateDistanceMeters(5f)
            .build()

        val callback = object : LocationCallback() {
            override fun onLocationResult(result: LocationResult) {
                result.lastLocation?.let { location ->
                    trySend(location)
                    // Also emit over WebSocket
                    socket.sendLocation(
                        latitude = location.latitude,
                        longitude = location.longitude,
                        accuracy = location.accuracy,
                        speed = location.speed
                    )
                }
            }
        }

        if (ContextCompat.checkSelfPermission(context, Manifest.permission.ACCESS_FINE_LOCATION)
            == PackageManager.PERMISSION_GRANTED
        ) {
            fusedLocationClient.requestLocationUpdates(request, callback, Looper.getMainLooper())
        }

        awaitClose { fusedLocationClient.removeLocationUpdates(callback) }
    }

    fun observePartnerLocation(): Flow<PartnerLocation> = socket.events
        .filterIsInstance<SocketEvent.PartnerLocationUpdate>()
        .map { PartnerLocation(lat = it.latitude, lng = it.longitude, updatedAt = it.timestamp) }
}

Step 2: ViewModel

@HiltViewModel
class LiveLocationViewModel @Inject constructor(
    private val repository: LiveLocationRepository,
    private val routingRepository: RoutingRepository
) : ViewModel() {

    private val _state = MutableStateFlow(LiveLocationState())
    val state: StateFlow<LiveLocationState> = _state.asStateFlow()

    init {
        observeMyLocation()
        observePartnerLocation()
    }

    private fun observeMyLocation() = viewModelScope.launch {
        repository.startSharing().collect { location ->
            _state.update { it.copy(myLocation = LatLng(location.latitude, location.longitude)) }
            // Recalculate ETA when my position changes significantly
            recomputeEta()
        }
    }

    private fun observePartnerLocation() = viewModelScope.launch {
        repository.observePartnerLocation().collect { partner ->
            _state.update { it.copy(
                partnerLocation = LatLng(partner.lat, partner.lng),
                partnerLastSeen = partner.updatedAt
            ) }
            recomputeEta()
        }
    }

    private var etaJob: Job? = null
    private fun recomputeEta() {
        val my = _state.value.myLocation ?: return
        val partner = _state.value.partnerLocation ?: return

        // Debounce ETA recalculation
        etaJob?.cancel()
        etaJob = viewModelScope.launch {
            delay(5_000)  // wait 5s before recalculating
            val result = runCatching { routingRepository.getRoute(my, partner) }
            result.onSuccess { route ->
                _state.update { it.copy(
                    etaSeconds = route.durationSeconds,
                    distanceMeters = route.distanceMeters
                ) }
            }
        }
    }
}

data class LiveLocationState(
    val myLocation: LatLng? = null,
    val partnerLocation: LatLng? = null,
    val partnerLastSeen: Long? = null,
    val etaSeconds: Long? = null,
    val distanceMeters: Int? = null
)

Step 3: Smooth Marker Animation

class MarkerAnimator(private val map: GoogleMap) {
    private var partnerMarker: Marker? = null

    fun animatePartnerTo(newPosition: LatLng) {
        val marker = partnerMarker
        if (marker == null) {
            // First update — place immediately
            partnerMarker = map.addMarker(
                MarkerOptions()
                    .position(newPosition)
                    .icon(BitmapDescriptorFactory.defaultMarker(BitmapDescriptorFactory.HUE_AZURE))
                    .title("Partner")
            )
            return
        }

        // Animate from current to new position
        val startPosition = marker.position
        ValueAnimator.ofFloat(0f, 1f).apply {
            duration = 1000  // 1s animation
            interpolator = LinearInterpolator()
            addUpdateListener { animator ->
                val fraction = animator.animatedFraction
                val interpolated = LatLng(
                    startPosition.latitude + (newPosition.latitude - startPosition.latitude) * fraction,
                    startPosition.longitude + (newPosition.longitude - startPosition.longitude) * fraction
                )
                marker.position = interpolated
            }
            start()
        }
    }
}

Step 4: Compose UI

@Composable
fun LiveLocationScreen(viewModel: LiveLocationViewModel = hiltViewModel()) {
    val state by viewModel.state.collectAsStateWithLifecycle()

    Column(Modifier.fillMaxSize()) {
        // Status bar
        state.etaSeconds?.let { eta ->
            Row(
                Modifier.fillMaxWidth().background(MaterialTheme.colorScheme.primaryContainer).padding(16.dp),
                horizontalArrangement = Arrangement.SpaceBetween
            ) {
                Text("ETA: ${formatEta(eta)}")
                Text("${state.distanceMeters?.let { "${it / 1000.0} km" } ?: ""}")
            }
        }

        // Partner staleness indicator
        state.partnerLastSeen?.let { lastSeen ->
            val ageSeconds = (System.currentTimeMillis() - lastSeen) / 1000
            if (ageSeconds > 30) {
                Text(
                    "Partner location ${ageSeconds}s old",
                    color = MaterialTheme.colorScheme.error,
                    modifier = Modifier.padding(8.dp)
                )
            }
        }

        // Map
        AndroidView(
            factory = { context ->
                MapView(context).apply {
                    onCreate(null)
                    onResume()
                }
            },
            modifier = Modifier.fillMaxSize()
        )
    }
}

private fun formatEta(seconds: Long): String {
    return when {
        seconds < 60 -> "< 1 min"
        seconds < 3600 -> "${seconds / 60} min"
        else -> "${seconds / 3600}h ${(seconds % 3600) / 60}m"
    }
}

Verification Checklist

[ ] My location marker moves as I walk
[ ] Partner location marker animates smoothly (no jump) between updates
[ ] ETA updates within 10 seconds of significant position change
[ ] "Location Xsec old" appears when partner goes offline > 30s
[ ] App in background: location still emits (if foreground service running)
[ ] WebSocket reconnects after network interruption

YOUR LEARNING JOURNEY

0 of 177 available lessons completed

Progress saved in this browser. No account needed.
Exercise: Live Location Demo | Android System Design | Android Engineers