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.

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 dataSuccess
when data is loadedError
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

🧩 Key Points
- Understand UI state management with sealed classes
- Show loading indicators using
CircularProgressIndicator
- Display error messages with retry capability
- Structure ViewModel with
StateFlow
orLiveData
- Build reusable UI state Composables
- Improve UX with responsive error-handling patterns
Suggested Internal Links
- Day 10: ViewModel in Jetpack Compose
- Day 12: StateFlow vs LiveData
- Day 21: JSON Parsing with Kotlin Serialization
- Day 19: CRUD Operations with Room
❓ FAQ
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.
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.
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.
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.
A: Instead of only using spinners, try placeholder animations like shimmer effects (e.g., using Accompanist’s placeholder()
modifier) to enhance perceived performance.