DEV Community

myougaTheAxo
myougaTheAxo

Posted on

Error Handling UI in Compose — Error, Loading & Empty State Patterns

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>()
}
Enter fullscreen mode Exit fullscreen mode

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)
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

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")
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

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")
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

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)
                )
            }
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

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")
            }
        )
    }
}
Enter fullscreen mode Exit fullscreen mode

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) }
                    )
                }
            }
        }
    )
}
Enter fullscreen mode Exit fullscreen mode

Build robust, user-friendly error experiences that guide users toward resolution!


Get 8 Android app templates: Gumroad

Top comments (0)