Implementing Retry with Timeout in Kotlin Coroutines

Mohamed Elsdody
3 min readMay 9, 2024

Handling network requests efficiently requires robust error handling and retry mechanisms, especially in mobile applications where network instability is common. Kotlin Coroutines provides a powerful way to manage asynchronous tasks with more readable and concise code compared to traditional callbacks and thread management. In this tutorial, we’ll explore how to implement a retry mechanism with a timeout using Kotlin Coroutines.

Overview

The goal is to retry a specific block of code a certain number of times with a timeout for each attempt. This is particularly useful for network requests that might occasionally fail due to temporary issues like network congestion.

Here’s a practical implementation using a ViewModel in an Android application:

Initial Setup

First, ensure you have the necessary dependencies in your build.gradle file:

dependencies {
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.0"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.0"
implementation 'com.jakewharton.timber:timber:5.0.1'
}

ViewModel Implementation

Below is the Kotlin code for a ViewModel that tries to fetch data about Android version features with a retry mechanism.


import androidx.lifecycle.viewModelScope
import base.BaseViewModel
import mock.MockApi
import kotlinx.coroutines.*
import timber.log.Timber

class TimeoutAndRetryViewModel(
private val api: MockApi = mockApi()
) : BaseViewModel<UiState>() {

fun performNetworkRequest() {
uiState.value = UiState.Loading
val numberOfRetries = 2
val timeout = 1000L

val oreoVersionsDeferred = viewModelScope.async {
retryWithTimeout(numberOfRetries, timeout) {
api.getAndroidVersionFeatures(27)
}
}

val pieVersionsDeferred = viewModelScope.async {
retryWithTimeout(numberOfRetries, timeout) {
api.getAndroidVersionFeatures(28)
}
}

viewModelScope.launch {
try {
val versionFeatures = listOf(
oreoVersionsDeferred,
pieVersionsDeferred
).awaitAll()

uiState.value = UiState.Success(versionFeatures)

} catch (e: Exception) {
Timber.e(e)
uiState.value = UiState.Error("Network Request failed")
}
}
}

private suspend fun <T> retryWithTimeout(
numberOfRetries: Int,
timeout: Long,
block: suspend () -> T
) = retry(numberOfRetries) {
withTimeout(timeout) {
block()
}
}

private suspend fun <T> retry(
numberOfRetries: Int,
delayBetweenRetries: Long = 100,
block: suspend () -> T
): T {
repeat(numberOfRetries) {
try {
return block()
} catch (exception: Exception) {
Timber.e(exception)
}
delay(delayBetweenRetries)
}
return block() // last attempt
}
}
open class BaseViewModel<T> : ViewModel() {

fun uiState(): LiveData<T> = uiState
protected val uiState: MutableLiveData<T> = MutableLiveData()
}
interface MockApi {

@GET("recent-android-versions")
suspend fun getRecentAndroidVersions(): List<AndroidVersion>

@GET("android-version-features/{apiLevel}")
suspend fun getAndroidVersionFeatures(@Path("apiLevel") apiLevel: Int): VersionFeatures
}

fun createMockApi(interceptor: MockNetworkInterceptor): MockApi {
val okHttpClient = OkHttpClient.Builder()
.addInterceptor(interceptor)
.build()

val retrofit = Retrofit.Builder()
.baseUrl("http://localhost/")
.client(okHttpClient)
.addConverterFactory(GsonConverterFactory.create())
.build()

return retrofit.create(MockApi::class.java)
}

Explanation

  1. ViewModel and State Management: We use BaseViewModel<UiState>() to manage UI states like loading, success, and error.
  2. performNetworkRequest: This function initiates the network requests. It uses viewModelScope to launch coroutines tied to the ViewModel's lifecycle.
  3. retryWithTimeout Function: This is a higher-order function that retries the given block of code (block) up to numberOfRetries times with a specified timeout for each attempt. If the block does not complete within the timeout, it will throw a TimeoutCancellationException.
  4. retry Function: This retries a block of code, catching any exceptions thrown within the block. If an exception is caught, it waits for delayBetweenRetries milliseconds before retrying.

Use Case

In the performNetworkRequest method, we fetch features of Android Oreo and Pie by launching asynchronous tasks. These tasks attempt to get data from a mock API and will retry up to two times with a 1-second timeout for each request.

Conclusion

This implementation of retry with timeout using Kotlin Coroutines is a robust solution for handling transient failures in network requests. By leveraging coroutines, we write code that is not only concise but also easy to read and maintain.

--

--

Mohamed Elsdody

🚀 Mobile Application Developer | Android & iOS Specialist | Cross-Platform Innovator (React Native , Flutter , KMP)