Bulletproof API Handling in Android: Survive Any Network with RetryInterceptor + WorkManager + Jetpack Compose


“What if your app never failed — even when the network did?”

You tap Submit.
You're on a weak 3G signal.
The request hangs... times out... and fails silently.
The user is frustrated.
The data is lost.

This is exactly what we’re going to fix today.


The Real-World Challenge

Most apps assume:

  • The user is online

  • The network is fast

  • The API request will succeed — or at least fail quickly

But real mobile users face:

  • Slow, unreliable connections

  • Network dropouts mid-request

  • Expired access tokens

  • Limited battery and data

Let’s build an Android app architecture that:

  • Detects slow internet before the API call

  • Retries intelligently using RetryInterceptor

  • Defers failures to WorkManager

  • Refreshes tokens using Mutex

  • Shows clean Compose UI feedback


The Hybrid Strategy: Retry Smart, Retry Forever

We'll combine:

RetryInterceptor

  • Fast, foreground retry

  • Handles transient failures (timeouts, connection issues)

  • Retries 3 times with exponential backoff

WorkManager

  • Deferred, background retry

  • Queues failed or slow requests

  • Reschedules automatically — even after app restarts

Together, they ensure API success is guaranteed — now or later.


Step 1: Detect Slow Internet Before Retrying

Why?

Why retry immediately if the internet is already too slow to succeed?

We'll run a small file download as a pre-flight check.

object NetworkSpeedChecker {
    suspend fun isSlowConnection(): Boolean {
        return withContext(Dispatchers.IO) {
            try {
                val start = System.currentTimeMillis()
                val url = URL("https://www.google.com/images/branding/googlelogo/2x/googlelogo_color_92x30dp.png")
                val conn = url.openConnection() as HttpURLConnection
                conn.connectTimeout = 3000
                conn.readTimeout = 3000

                val input = conn.inputStream
                val buffer = ByteArray(1024)
                var total = 0
                while (input.read(buffer) != -1 && total < 100_000) {
                    total += buffer.size
                }

                val duration = System.currentTimeMillis() - start
                duration > 3000
            } catch (e: Exception) {
                true
            }
        }
    }
}

If this takes > 3 seconds, we skip the API and fallback to WorkManager.


Step 2: RetryInterceptor — Foreground Fast Retry

What It Solves

  • Temporary network hiccups

  • Timeouts from slow DNS or low signal

Code

class RetryInterceptor(private val maxRetries: Int = 3) : Interceptor {
    override fun intercept(chain: Interceptor.Chain): Response {
        var attempt = 0
        while (true) {
            try {
                return chain.proceed(chain.request())
            } catch (e: IOException) {
                if (attempt >= maxRetries - 1) throw e
                Thread.sleep(1000L * (attempt + 1))
                attempt++
            }
        }
    }
}

Usage

val okHttpClient = OkHttpClient.Builder()
    .addInterceptor(RetryInterceptor())
    .build()

Step 3: WorkManager — Deferred Retry That Survives Anything

What It Solves

  • Slow networks

  • Total network dropouts

  • Long failures across app kills or reboots

class FeedbackWorker(context: Context, workerParams: WorkerParameters) :
    CoroutineWorker(context, workerParams) {

    override suspend fun doWork(): Result {
        val json = inputData.getString("payload") ?: return Result.failure()
        val feedback = Gson().fromJson(json, Feedback::class.java)
        return try {
            val response = ApiClient.api.sendFeedback(feedback)
            if (response.isSuccessful) Result.success() else Result.retry()
        } catch (e: IOException) {
            Result.retry()
        }
    }
}

Enqueue WorkManager Job

fun enqueueWork(context: Context, feedback: Feedback) {
    val data = workDataOf("payload" to Gson().toJson(feedback))

    val request = OneTimeWorkRequestBuilder<FeedbackWorker>()
        .setInputData(data)
        .setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 15, TimeUnit.SECONDS)
        .setConstraints(
            Constraints.Builder()
                .setRequiredNetworkType(NetworkType.CONNECTED)
                .build()
        )
        .build()

    WorkManager.getInstance(context).enqueue(request)
}

Step 4: ViewModel with Smart Retry Logic

class FeedbackViewModel : ViewModel() {
    var state by mutableStateOf<ResultState>(ResultState.Idle)

    fun sendFeedback(context: Context, feedback: Feedback) {
        viewModelScope.launch {
            state = ResultState.Loading

            if (NetworkSpeedChecker.isSlowConnection()) {
                enqueueWork(context, feedback)
                state = ResultState.SlowNetwork
                return@launch
            }

            try {
                val response = ApiClient.api.sendFeedback(feedback)
                state = if (response.isSuccessful) {
                    ResultState.Success
                } else {
                    enqueueWork(context, feedback)
                    ResultState.Fallback
                }
            } catch (e: IOException) {
                enqueueWork(context, feedback)
                state = ResultState.Fallback
            }
        }
    }
}

Step 5: Compose UI — Smart Feedback for Smart Apps

@Composable
fun FeedbackScreen(viewModel: FeedbackViewModel = viewModel()) {
    val state = viewModel.state

    when (state) {
        is ResultState.Loading -> CircularProgressIndicator()
        is ResultState.Success -> Text("Feedback submitted!")
        is ResultState.SlowNetwork -> Text("Slow internet. Retrying in background.")
        is ResultState.Fallback -> Text("Retrying in background...")
        is ResultState.Error -> {
            Text("Error: ${state.message}")
            Button(onClick = { viewModel.sendFeedback(...) }) {
                Text("Retry Now")
            }
        }
        else -> {}
    }
}

Step 6: AuthInterceptor with Suspend-Safe Token Refresh

class AuthInterceptor(private val tokenManager: TokenManager) : Interceptor {
    private val mutex = Mutex()

    override fun intercept(chain: Interceptor.Chain): Response {
        var request = chain.request()
        var response = chain.proceed(request)

        if (response.code == 401) {
            mutex.withLock {
                val newToken = runBlocking { tokenManager.refreshToken() }
                tokenManager.saveToken(newToken)

                response.close()
                request = request.newBuilder()
                    .header("Authorization", "Bearer $newToken")
                    .build()
                response = chain.proceed(request)
            }
        }

        return response
    }
}

Result State Model

sealed class ResultState {
    object Idle : ResultState()
    object Loading : ResultState()
    object Success : ResultState()
    object SlowNetwork : ResultState()
    object Fallback : ResultState()
    data class Error(val message: String) : ResultState()
}

✅ 10 Real Benefits

Benefit Why it matters
Fast retry Fixes hiccups without bothering users
Slow net detection Avoids guaranteed failure and waste
Background reschedule Handles complete network loss or app crash
Token refresh safe Uses Mutex to avoid duplication
Composable UI feedback Keeps users informed at every stage
Battery & system-aware Uses WorkManager smartly
Easy to scale Add upload, sync, or more types of jobs
Clean MVVM Easy to test and maintain
Offline-first friendly Retry later when online
Production ready Real-world tested strategy for large apps

❓ Q&A by Experience Level

Beginner

Q: Do I need to check for internet manually?
A: No, just use the speed checker before calling the API.

Q: Will this crash if network fails?
A: Nope. All failures are gracefully caught and handled.


Intermediate

Q: Can I customize the backoff?
A: Yes! setBackoffCriteria() lets you control retry timing.

Q: Can I extend this to other features?
A: Easily. Wrap payloads and send them to workers.


Advanced

Q: How do I test all this?
A: Use mock APIs, WorkManagerTestInitHelper, and test ViewModels with CoroutineDispatcher.

Q: Can I make this generic for all APIs?
A: Absolutely — abstract to a BaseRetryWorker and build on top.


Conclusion: This Is Real-World API Reliability

With this architecture:

  • You recover quickly from hiccups

  • You fallback gracefully when the network is bad

  • You survive failures, crashes, and offline states

Your app becomes trustworthy, resilient, and production-grade.


Want the Code?

I’m building a plug-and-play starter repo (Kotlin + Compose + Retrofit + WorkManager + Hilt).
Drop a comment or DM me and I’ll send it to you!


Follow me for:

  • Jetpack Compose Architecture

  • Real-world MVVM + Coroutine Patterns

  • Scalable Android development


Comments

Popular posts from this blog

Jetpack Compose based Android interview and questions

Kotlin Some important unsorted interview questions and answers

Null safety based Kotlin interview questions and answers