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
Post a Comment