"Retry When Ready" — A Smarter Way to Handle Network Failures in Jetpack Compose

 


"Please try again."

These are the words users dread to see — especially when they’ve done nothing wrong except have poor internet.

In this post, I’ll show you how to build a reusable, respectful retry system in Jetpack Compose that:

  • Remembers failed API requests
  • Waits for internet to return
  • Gently asks the user: “Would you like to retry?”
  • Works not just for login — but any network-dependent call

Let’s build this like a good story: with empathy, architecture, and a clean separation of concerns.


The Problem: Network Failures Break Trust

Imagine this:

  1. A user opens your app and enters their login credentials.

  2. They tap Log In.

  3. Nothing happens — or worse, an error appears:
    “Login failed. Please try again.”

Why?
No internet. Maybe they walked through a tunnel. Maybe their Wi-Fi dropped.

Now imagine if the app said:

“You're back online. Would you like to retry logging in?”

Boom. That's thoughtful UX.


The Goal

We want to create a centralized retry mechanism that:

  • Works for any failed API

  • Remembers the user's action

  • Waits for the network

  • Gives control back to the user via a dialog

  • Is reusable across all ViewModels and screens

Let’s do it.


The Architecture

We already had a basic UiEvent system in our app. We enhanced it like this:

sealed class UiEvent {
    data class ShowAlert(val title: String, val message: String) : UiEvent()
    object ShowLoader : UiEvent()
    object HideLoader : UiEvent()

    data class ShowRetryDialog(
        val title: String,
        val message: String,
        val retryAction: RetryAction
    ) : UiEvent()

    enum class RetryAction {
        LOGIN,
        FETCH_PROFILE,
        SEND_OTP
        // add more as needed
    }
}

Then, we used polymorphism to make this generic and reusable.


Smart ViewModels

 In LoginViewModel

private var lastLoginRequest: LoginRequest? = null
private var isRetryPending = false

fun loginUser(request: LoginRequest) {
    lastLoginRequest = request
    isRetryPending = false
    // Trigger login API
}

fun markLoginRetryNeeded() {
    isRetryPending = true
}

fun promptLoginRetryOnNetworkRestored() {
    if (isRetryPending) {
        showRetryDialog(
            title = "Internet Restored",
            message = "Do you want to retry login?",
            action = UiEvent.RetryAction.LOGIN
        )
    }
}

override fun handleRetryAction(action: UiEvent.RetryAction) {
    when (action) {
        UiEvent.RetryAction.LOGIN -> retryLogin()
    }
}

private fun retryLogin() {
    lastLoginRequest?.let { loginUser(it) }
}

In the Base AppUiViewModel

open fun handleRetryAction(action: UiEvent.RetryAction) {
    // No-op by default
}

Now, any child ViewModel can override only what it needs


The Magic: UiEventHandler

This composable is used by all screens to handle events like loaders, alerts, and now… retries!

retryDialog?.let { dialog ->
    AlertDialog(
        onDismissRequest = { retryDialog = null },
        title = { Text(dialog.title) },
        text = { Text(dialog.message) },
        confirmButton = {
            TextButton(onClick = {
                appUiViewModel.handleRetryAction(dialog.retryAction)
                retryDialog = null
            }) {
                Text("Retry")
            }
        },
        dismissButton = {
            TextButton(onClick = {
                retryDialog = null
            }) {
                Text("Cancel")
            }
        }
    )
}

It doesn't care what the retry action is — it simply delegates.


Scaling the Pattern

Want to use this for more than login?

Here’s how:

1 Add a new enum entry:

enum class RetryAction {
    LOGIN,
    FETCH_PROFILE,
    SEND_OTP
}

2 In your ProfileViewModel:

override fun handleRetryAction(action: UiEvent.RetryAction) {
    when (action) {
        UiEvent.RetryAction.FETCH_PROFILE -> fetchProfile(lastRequest)
    }
}

3 In your repo:

fun fetchProfile(request: ProfileRequest) {
    lastRequest = request
    // API call
}

Now, any ViewModel can opt in. No code duplication. No tight coupling.


Real-World Experience

This approach has made our app:

  • More user-friendly 

  • More resilient 

  • More scalable 

We no longer fear network drops. Instead, we anticipate them. We recover from them.
And more importantly — we empower the user to decide.


Want More Like This?

If you found this helpful:

Clap 👏 a few times

Follow me on Medium

Subscribe for email alerts to get real-world Compose & architecture tips delivered to your inbox

Let’s build smarter, friendlier apps — one retry at a time.



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