Developing Order Status Tracking in Mobile Apps
User opens the app 40 minutes after placing an order and sees "Processing" status — same as right after payment. Calls support. The problem isn't logistics: the courier is already driving, coordinates update on the server every 30 seconds. The problem is the mobile app isn't connected to this stream.
Status timeline and courier map are two different mechanisms with different update requirements, and mixing them in one polling request is the first architectural mistake.
Status timeline: WebSocket vs polling
Order status changes rarely — 5-7 times across the entire lifecycle. For this, long-polling or SSE (Server-Sent Events) fits: connection is open, server pushes event only on status change. WebSocket is overkill here, though often chosen by inertia.
On iOS, SSE implementation via URLSession looks like this:
let request = URLRequest(url: URL(string: "https://api.example.com/orders/\(orderId)/status-stream")!)
let task = URLSession.shared.dataTask(with: request) { data, response, error in
// parse text/event-stream line by line
}
task.resume()
Better to use a ready-made library — IVALiveEventSource or Swift Package swift-eventsource from LaunchDarkly. They correctly handle reconnect and heartbeat.
On Android — OkHttp with EventSource from library com.launchdarkly:okhttp-eventsource. Writing native HttpURLConnection for SSE manually — waste of time on edge cases.
Timeline structure on screen
For displaying status progress, use RecyclerView (Android) or UICollectionView with custom layout (iOS). Typical mistake — storing "passed" statuses only on client. If user deleted and reinstalled app, history is gone. All completed statuses with timestamps must be returned from server in an array:
{
"currentStatus": "courier_assigned",
"timeline": [
{ "status": "created", "timestamp": "2024-03-15T10:00:00Z" },
{ "status": "confirmed", "timestamp": "2024-03-15T10:02:30Z" },
{ "status": "courier_assigned", "timestamp": "2024-03-15T10:15:00Z" }
]
}
Courier map: separate data stream
Courier coordinates update every 15-30 seconds — this is already WebSocket or separate polling with short interval. Mixing it with status stream in one endpoint means either overloading the status stream or updating the map too rarely.
In practice, do it this way:
- Statuses — SSE or push notifications (Firebase Cloud Messaging)
- Coordinates — WebSocket with 15-30 second interval or polling
/orders/{id}/courier-location
Smoothing courier marker movement
Courier marker jumps on the map if you just set new coordinates directly. Correct — animate movement between points. On Android via ValueAnimator:
val animator = ValueAnimator.ofFloat(0f, 1f).apply {
duration = 1000
addUpdateListener { animation ->
val fraction = animation.animatedValue as Float
val lat = startLat + (endLat - startLat) * fraction
val lng = startLng + (endLng - startLng) * fraction
courierMarker.position = LatLng(lat, lng)
}
}
animator.start()
On iOS via CADisplayLink or UIView.animate with intermediate coordinates.
Rotating courier icon by movement direction calculated via atan2(deltaLat, deltaLng) — don't forget to convert radians to degrees for marker.rotation.
Push notifications as fallback channel
User minimizes app — WebSocket and SSE break. Key status changes (courier picked up order, courier nearby, order delivered) duplicated via FCM/APNs. On iOS need UNUserNotificationCenter, on Android — FirebaseMessagingService.
Nuance: "courier nearby" notification loses meaning if it arrives 10 minutes after delivery. Server must check event relevance before sending push — this is server logic, not mobile.
What's included
- Status timeline with SSE or FCM pushes
- Courier map with animated marker (Google Maps SDK or MapKit)
- WebSocket or polling for coordinates with proper lifecycle (onPause/onResume / viewDidDisappear)
- Offline state handling: event queue and sync on network recovery
Timeline
3–5 days for full flow: timeline + map + pushes. Timeline only without map — 1–2 days. Price calculated individually after requirements analysis.







