Master error handling in Compose with sealed classes, loading states, and retry mechanisms.
Sealed UiState Pattern
sealed class UiState<out T> {
data class Loading(val isRefreshing: Boolean = false) : UiState<Nothing>()
data class Success<T>(val data: T) : UiState<T>()
data class Error(val exception: Throwable, val retryAction: (() -> Unit)? = null) : UiState<Nothing>()
data class Empty(val message: String = "No data available") : UiState<Nothing>()
}
StatefulContent Composable for State Switching
@Composable
fun <T> StatefulContent(
uiState: UiState<T>,
onRetry: () -> Unit = {},
successContent: @Composable (T) -> Unit,
modifier: Modifier = Modifier
) {
when (uiState) {
is UiState.Loading -> {
LoadingScreen(isRefreshing = uiState.isRefreshing, modifier = modifier)
}
is UiState.Success -> {
successContent(uiState.data)
}
is UiState.Error -> {
ErrorScreen(
exception = uiState.exception,
onRetry = { uiState.retryAction?.invoke() },
modifier = modifier
)
}
is UiState.Empty -> {
EmptyScreen(message = uiState.message, modifier = modifier)
}
}
}
Error View with Icon, Message & Retry Button
@Composable
fun ErrorScreen(
exception: Throwable,
onRetry: () -> Unit,
modifier: Modifier = Modifier
) {
Column(
modifier = modifier
.fillMaxSize()
.padding(24.dp),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Icon(
imageVector = Icons.Default.ErrorOutline,
contentDescription = "Error",
modifier = Modifier
.size(80.dp)
.padding(bottom = 16.dp),
tint = MaterialTheme.colorScheme.error
)
Text(
text = "Oops! Something went wrong",
style = MaterialTheme.typography.headlineSmall,
textAlign = TextAlign.Center,
modifier = Modifier.padding(bottom = 8.dp)
)
Text(
text = exception.message ?: "Unknown error",
style = MaterialTheme.typography.bodyMedium,
textAlign = TextAlign.Center,
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.padding(bottom = 24.dp)
)
Button(
onClick = onRetry,
modifier = Modifier
.fillMaxWidth()
.height(48.dp)
) {
Icon(
imageVector = Icons.Default.Refresh,
contentDescription = null,
modifier = Modifier.padding(end = 8.dp)
)
Text("Retry")
}
}
}
Empty State View with Action Button
@Composable
fun EmptyScreen(
message: String,
onAction: () -> Unit = {},
modifier: Modifier = Modifier
) {
Column(
modifier = modifier
.fillMaxSize()
.padding(24.dp),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Icon(
imageVector = Icons.Default.InboxOutlined,
contentDescription = "Empty",
modifier = Modifier
.size(80.dp)
.padding(bottom = 16.dp),
tint = MaterialTheme.colorScheme.onSurfaceVariant
)
Text(
text = message,
style = MaterialTheme.typography.headlineSmall,
textAlign = TextAlign.Center,
modifier = Modifier.padding(bottom = 16.dp)
)
OutlinedButton(
onClick = onAction,
modifier = Modifier
.fillMaxWidth()
.height(48.dp)
) {
Icon(
imageVector = Icons.Default.Add,
contentDescription = null,
modifier = Modifier.padding(end = 8.dp)
)
Text("Create New")
}
}
}
Loading State Screen
@Composable
fun LoadingScreen(
isRefreshing: Boolean = false,
modifier: Modifier = Modifier
) {
Box(
modifier = modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
if (isRefreshing) {
CircularProgressIndicator(
modifier = Modifier.size(48.dp)
)
} else {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
CircularProgressIndicator(
modifier = Modifier.size(64.dp)
)
Text(
text = "Loading...",
style = MaterialTheme.typography.bodyMedium,
modifier = Modifier.padding(top = 16.dp)
)
}
}
}
}
Snackbar Error Notification with Retry Action
@Composable
fun ScreenWithSnackbarError(
viewModel: MyViewModel
) {
val snackbarHostState = remember { SnackbarHostState() }
val uiState by viewModel.uiState.collectAsState()
LaunchedEffect(uiState) {
if (uiState is UiState.Error) {
val error = (uiState as UiState.Error)
snackbarHostState.showSnackbar(
message = error.exception.message ?: "Unknown error",
actionLabel = "Retry",
duration = SnackbarDuration.Long
).let { result ->
if (result == SnackbarResult.ActionPerformed) {
error.retryAction?.invoke()
}
}
}
}
Scaffold(
snackbarHost = { SnackbarHost(snackbarHostState) }
) { padding ->
StatefulContent(
uiState = uiState,
modifier = Modifier.padding(padding),
successContent = { data ->
// Your success content
Text("Success: $data")
}
)
}
}
Complete Screen Example
@Composable
fun DataListScreen(
viewModel: DataViewModel = hiltViewModel()
) {
val uiState by viewModel.uiState.collectAsState()
StatefulContent(
uiState = uiState,
onRetry = { viewModel.loadData() },
successContent = { data ->
LazyColumn(
modifier = Modifier
.fillMaxSize()
.padding(16.dp)
) {
items(data) { item ->
ListItem(
headlineContent = { Text(item.title) },
supportingContent = { Text(item.description) }
)
}
}
}
)
}
Build robust, user-friendly error experiences that guide users toward resolution!
Get 8 Android app templates: Gumroad
Top comments (0)