Driver-rider matching is the core of any ride-share app. The client must display live driver locations, animate movement smoothly, show ETA updates in realtime, and handle the state machine from request to dropoff.
Matching State Machine
enum class RideStatus {
IDLE, // no active request
REQUESTING, // user submitted request; waiting for match
DRIVER_FOUND, // matched — driver on the way
DRIVER_ARRIVED, // driver at pickup location
IN_TRIP, // trip in progress
COMPLETED, // arrived at destination
CANCELLED // cancelled by driver or rider
}
data class RideState(
val status: RideStatus = RideStatus.IDLE,
val driverId: String? = null,
val driverName: String? = null,
val driverAvatarUrl: String? = null,
val driverLocation: LatLng? = null,
val vehicleType: String? = null,
val licensePlate: String? = null,
val etaSeconds: Int? = null,
val tripRoute: List<LatLng> = emptyList()
)
Realtime Driver Location via WebSocket
class RideWebSocket @Inject constructor(
private val okHttpClient: OkHttpClient
) {
private var webSocket: WebSocket? = null
private val _events = MutableSharedFlow<RideEvent>(replay = 0)
val events: SharedFlow<RideEvent> = _events
fun connect(rideId: String) {
val request = Request.Builder()
.url("wss://api.example.com/rides/$rideId/stream")
.build()
webSocket = okHttpClient.newWebSocket(request, object : WebSocketListener() {
override fun onMessage(webSocket: WebSocket, text: String) {
val event = parseRideEvent(text)
_events.tryEmit(event)
}
override fun onFailure(webSocket: WebSocket, t: Throwable, response: Response?) {
_events.tryEmit(RideEvent.ConnectionError(t.message ?: "Unknown error"))
scheduleReconnect(rideId)
}
})
}
private fun scheduleReconnect(rideId: String) {
// Exponential backoff reconnect
}
fun disconnect() { webSocket?.close(1000, "Ride completed") }
}
sealed class RideEvent {
data class DriverLocationUpdate(val location: LatLng, val heading: Float) : RideEvent()
data class EtaUpdate(val seconds: Int) : RideEvent()
data class StatusChange(val status: RideStatus) : RideEvent()
data class ConnectionError(val message: String) : RideEvent()
}
Smooth Driver Marker Animation
class DriverMarkerAnimator(private val marker: Marker) {
fun animateToPosition(target: LatLng, heading: Float) {
val start = marker.position
val valueAnimator = ValueAnimator.ofFloat(0f, 1f).apply {
duration = 800 // match GPS update interval
interpolator = LinearInterpolator()
addUpdateListener { animator ->
val fraction = animator.animatedFraction
val lat = start.latitude + (target.latitude - start.latitude) * fraction
val lng = start.longitude + (target.longitude - start.longitude) * fraction
marker.position = LatLng(lat, lng)
}
}
valueAnimator.start()
// Animate rotation separately for smooth bearing change
val rotationAnimator = ObjectAnimator.ofFloat(marker.rotation, heading).apply {
duration = 500
addUpdateListener { marker.rotation = it.animatedValue as Float }
}
rotationAnimator.start()
}
}
ViewModel: Orchestrating the Ride
@HiltViewModel
class RideViewModel @Inject constructor(
private val rideApi: RideApi,
private val rideWebSocket: RideWebSocket
) : ViewModel() {
private val _state = MutableStateFlow(RideState())
val state: StateFlow<RideState> = _state
fun requestRide(pickup: LatLng, destination: LatLng, vehicleType: String) {
_state.value = _state.value.copy(status = RideStatus.REQUESTING)
viewModelScope.launch {
try {
val ride = rideApi.requestRide(pickup, destination, vehicleType)
_state.value = _state.value.copy(
status = RideStatus.DRIVER_FOUND,
driverId = ride.driverId,
driverName = ride.driverName,
driverLocation = ride.driverLocation,
etaSeconds = ride.etaSeconds,
licensePlate = ride.licensePlate
)
connectToRideStream(ride.rideId)
} catch (e: Exception) {
_state.value = _state.value.copy(status = RideStatus.IDLE)
}
}
}
private fun connectToRideStream(rideId: String) {
rideWebSocket.connect(rideId)
viewModelScope.launch {
rideWebSocket.events.collect { event ->
when (event) {
is RideEvent.DriverLocationUpdate ->
_state.value = _state.value.copy(driverLocation = event.location)
is RideEvent.EtaUpdate ->
_state.value = _state.value.copy(etaSeconds = event.seconds)
is RideEvent.StatusChange ->
_state.value = _state.value.copy(status = event.status)
is RideEvent.ConnectionError -> { /* reconnect */ }
}
}
}
}
override fun onCleared() {
rideWebSocket.disconnect()
super.onCleared()
}
}
Key Takeaways
| Pattern | Rule |
|---|---|
| State machine | Model ride as an enum; every UI decision flows from RideStatus |
| WebSocket for location | GPS updates every 2–5s; WebSocket push is the right transport |
| Animate, don't jump | ValueAnimator over 800ms interpolates between GPS positions |
| Heading animation | Rotate marker smoothly with ObjectAnimator to match travel direction |
| Reconnect on error | Exponential backoff; never leave user staring at stale position |
| Cancel on viewModel cleared | Always disconnect() in onCleared(); prevent leak |