Day 22: Showing Loading and Error States in Jetpack Compose

Introduction

In modern Android app development, user experience is everything. One key area where developers often struggle is gracefully handling loading and error states—especially when working with remote APIs or databases. With Jetpack Compose, Android’s modern declarative UI toolkit, handling these states is not only more intuitive but also cleaner.

This guide shows you how to effectively implement loading and error states using sealed class, StateFlow, and Composables in Jetpack Compose.


Flow diagram showcasing UI states including Loading, Success, and Error handled with State and ViewModel in Jetpack Compose

Why UI State Management Matters

Every app encounters uncertain data conditions—be it due to:

  • Network latency
  • Backend errors
  • Empty responses
  • User-triggered failures

Properly handling these conditions ensures your app feels polished and responsive.


Define a Sealed Class for UI States

Using a sealed class to model your UI state keeps things organized and easily expandable.

kotlinCopyEditsealed class UiState<out T> {
    object Loading : UiState<Nothing>()
    data class Success<T>(val data: T) : UiState<T>()
    data class Error(val message: String) : UiState<Nothing>()
}

This gives you a clear structure:

  • Loading while fetching data
  • Success when data is loaded
  • Error if something goes wrong

ViewModel with StateFlow

Using StateFlow gives us a reactive stream of UI state changes, suitable for Jetpack Compose.

kotlinCopyEditclass UserViewModel : ViewModel() {

    private val _uiState = MutableStateFlow<UiState<List<String>>>(UiState.Loading)
    val uiState: StateFlow<UiState<List<String>>> = _uiState

    init {
        fetchUsers()
    }

    fun fetchUsers() {
        viewModelScope.launch {
            try {
                _uiState.value = UiState.Loading
                delay(2000) // Simulate network delay
                _uiState.value = UiState.Success(listOf("Alice", "Bob", "Charlie"))
            } catch (e: Exception) {
                _uiState.value = UiState.Error("Failed to load users")
            }
        }
    }
}

Main Screen with UI State Handling

Now create the Composable that reacts to UiState.

kotlinCopyEdit@Composable
fun UserScreen(viewModel: UserViewModel = viewModel()) {
    val state by viewModel.uiState.collectAsState()

    when (state) {
        is UiState.Loading -> LoadingView()
        is UiState.Success -> UserList((state as UiState.Success<List<String>>).data)
        is UiState.Error -> ErrorView(
            message = (state as UiState.Error).message,
            onRetry = { viewModel.fetchUsers() }
        )
    }
}

Building Composables for UI States

Loading View

kotlinCopyEdit@Composable
fun LoadingView() {
    Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
        CircularProgressIndicator()
    }
}

Error View with Retry Button

kotlinCopyEdit@Composable
fun ErrorView(message: String, onRetry: () -> Unit) {
    Column(
        modifier = Modifier
            .fillMaxSize()
            .padding(16.dp),
        verticalArrangement = Arrangement.Center,
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        Text(text = message, color = Color.Red)
        Spacer(modifier = Modifier.height(8.dp))
        Button(onClick = onRetry) {
            Text("Retry")
        }
    }
}

Success View (User List)

kotlinCopyEdit@Composable
fun UserList(users: List<String>) {
    LazyColumn {
        items(users) { user ->
            Text(text = user, modifier = Modifier.padding(8.dp))
        }
    }
}

Bonus: Enhancing with Shimmer Placeholder

You can use libraries like Accompanist Placeholder to show shimmer effects while loading, instead of plain spinners.

kotlinCopyEdit@Composable
fun ShimmerUserList(isLoading: Boolean) {
    LazyColumn {
        items(5) {
            Text(
                text = "",
                modifier = Modifier
                    .fillMaxWidth()
                    .height(20.dp)
                    .placeholder(visible = isLoading, highlight = PlaceholderHighlight.shimmer())
                    .padding(8.dp)
            )
        }
    }
}

Best Practices

  • Keep UiState sealed class generic and reusable
  • Avoid logic in Composables—delegate to ViewModel
  • Always offer a retry path on error
  • Use placeholders for better perceived performance
  • Decouple UI state management from navigation logic

Infographic highlighting key practices like using State, ViewModel, try-catch blocks, and user feedback techniques for error handling in Jetpack Compose

🧩 Key Points

  • Understand UI state management with sealed classes
  • Show loading indicators using CircularProgressIndicator
  • Display error messages with retry capability
  • Structure ViewModel with StateFlow or LiveData
  • Build reusable UI state Composables
  • Improve UX with responsive error-handling patterns

Suggested Internal Links

FAQ

Q1. How do I show loading state in Jetpack Compose?

A: Use CircularProgressIndicator inside a Box or Column to center it. Wrap it in a when statement driven by a UiState.Loading condition from your ViewModel.

Q2. How do I handle API errors in Jetpack Compose?

A: Create a UiState.Error class in your sealed UI state and show an error message with a Retry button when it occurs. Connect the Retry button to a method in your ViewModel to refetch the data.

Q3. What is the best way to manage UI state in Jetpack Compose?

A: Use StateFlow in your ViewModel to manage and expose UI state changes. Pair it with a sealed class like UiState<out T> to represent Loading, Success, and Error states cleanly.

Q4. Can I reuse UI states across different Composables?

A: Yes, using a sealed class like UiState allows you to standardize and reuse the same loading/error/success patterns across multiple screens or modules.

Q5. How do I improve UX during loading states?

A: Instead of only using spinners, try placeholder animations like shimmer effects (e.g., using Accompanist’s placeholder() modifier) to enhance perceived performance.

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top