On this page
The feature: an order-tracking screen that loads the order, its shipment status and recommended products, three endpoints with different reliability. The goal: fast loading, graceful partial failure, and code that's easy to test.
The end-to-end flow
1. The API contract and DTOs
interface OrderApi {
@GET("orders/{id}") suspend fun order(@Path("id") id: String): OrderDto
@GET("shipments/{orderId}") suspend fun shipment(@Path("orderId") id: String): ShipmentDto
@GET("recommendations") suspend fun recommendations(@Query("orderId") id: String): List<ProductDto>
}
@Serializable data class OrderDto(val id: String, val total_paise: Long, val placed_at: String)
@Serializable data class ShipmentDto(val status: String, val eta: String? = null)
@Serializable data class ProductDto(val id: String, val name: String)2. Domain models and mapping
enum class ShipmentStatus { PACKED, SHIPPED, OUT_FOR_DELIVERY, DELIVERED, UNKNOWN }
data class OrderDetails(
val orderId: String,
val totalPaise: Long,
val placedAt: Instant,
val status: ShipmentStatus,
val eta: LocalDate?,
val recommendations: List<Product>,
)
fun ShipmentDto.statusEnum() = runCatching { ShipmentStatus.valueOf(status) }.getOrDefault(ShipmentStatus.UNKNOWN)Unknown server values map to UNKNOWN instead of crashing, so a new server status doesn't break old app versions.
3. Repository: parallel, with partial failure
class OrderRepository(private val api: OrderApi) {
suspend fun details(orderId: String): OrderDetails = coroutineScope {
val order = async { retryIo { api.order(orderId) } }
val shipment = async { retryIo { api.shipment(orderId) } }
// Optional: failure shouldn't cancel siblings or fail the whole call
val recs = async {
runCatching { withTimeout(3_000) { api.recommendations(orderId) } }
.getOrDefault(emptyList())
}
val o = order.await()
val s = shipment.await()
OrderDetails(
orderId = o.id,
totalPaise = o.total_paise,
placedAt = Instant.parse(o.placed_at),
status = s.statusEnum(),
eta = s.eta?.let(LocalDate::parse),
recommendations = recs.await().map { Product(it.id, it.name) },
)
}
}
/** Retries transient I/O failures with exponential backoff; rethrows the last error. */
suspend fun <T> retryIo(times: Int = 3, initialDelay: Long = 400, block: suspend () -> T): T {
var delayMs = initialDelay
repeat(times - 1) {
try { return block() } catch (e: IOException) { delay(delayMs); delayMs *= 2 }
}
return block()
}runCatching also catches CancellationException. Here it wraps a block with its own timeout, and if the parent is cancelled, the following await() calls still observe cancellation. In general, prefer catching specific exceptions.
4. ViewModel state
sealed interface OrderUiState {
data object Loading : OrderUiState
data class Content(val details: OrderDetails) : OrderUiState
data class Error(val message: String) : OrderUiState
}
class OrderViewModel(private val repo: OrderRepository, savedState: SavedStateHandle) : ViewModel() {
private val orderId: String = checkNotNull(savedState["orderId"])
private val _state = MutableStateFlow<OrderUiState>(OrderUiState.Loading)
val state = _state.asStateFlow()
init { load() }
fun load() = viewModelScope.launch {
_state.value = OrderUiState.Loading
_state.value = try {
OrderUiState.Content(repo.details(orderId))
} catch (e: IOException) {
OrderUiState.Error("You're offline. Pull to retry.")
} catch (e: HttpException) {
OrderUiState.Error(if (e.code() == 404) "Order not found." else "Something went wrong (${e.code()}).")
}
}
}5. Testing the repository
@Test
fun `recommendations failure does not fail details`() = runTest {
val api = object : OrderApi {
override suspend fun order(id: String) = OrderDto(id, 129_900, "2026-10-01T10:00:00Z")
override suspend fun shipment(id: String) = ShipmentDto("SHIPPED", "2026-10-03")
override suspend fun recommendations(id: String): List<ProductDto> = throw IOException("down")
}
val details = OrderRepository(api).details("A1")
assertEquals(ShipmentStatus.SHIPPED, details.status)
assertTrue(details.recommendations.isEmpty())
}Variations
Dependent calls
Sequential: get the user, then fetch orders by the user's account ID. No async needed.
Polling a status
A flow { while (true) { emit(api.status()); delay(10_000) } } collected only while the screen is visible.
Offline support
Write results to Room and observe from there. See Offline-First.
Common mistakes
- Sequential calls for independent data, which doubles or triples loading time.
- Retrying non-idempotent requests, such as creating a payment, without idempotency keys.
- Retrying 4xx errors. Only transient failures (I/O, 5xx, 429 with backoff) deserve retries.
- Parsing dates and enums without fallbacks.
Frequently asked questions
coroutineScope or supervisorScope?
coroutineScope fails fast when any child fails, which is right for required data. supervisorScope lets children fail independently. Above, the optional call handles its own failure instead.
Where do timeouts belong?
Set global timeouts on OkHttp, and use withTimeout for specific, latency-sensitive optional calls.
Should I return Result from repositories?
Either convention works. Pick one per codebase. This example throws and maps in the ViewModel.
Conclusion
Good API integration combines clean DTO-to-domain mapping, parallel calls for independent data, isolated optional failures, retries only for transient errors, and explicit UI states, all testable with fakes.
Next step: compare HTTP clients in Retrofit vs Ktor Client.
Official references
๐ฑ Apps built by Yunok Tech
Real Android apps on Google Play, free to download.
Walzi4K wallpapers for lock and home screenGet it โธ
NotiVaultNotification history: recover dismissed notificationsGet it โธ
Ready to build real Android applications?
Networking, persistence and offline sync are where real apps get complicated. Practise building a data layer that handles slow networks, failures and caching, not just the happy path.
Move beyond theory and strengthen your Android development skills with Yunoktech's Android course. Learn core concepts, practical implementation, modern Android development techniques, and the reasoning behind building maintainable applications.
Explore the Android course and take your next step as an Android developer.