How to Handle Loading, Error, and Empty States in Jetpack Compose: A Complete Guide
The GSYGitHubApp manages transient UI states through a centralized GSYGeneralLoadState composable that declaratively switches between loading spinners, error messages with retry actions, and main content based on simple Boolean and nullable parameters.
The carguo/gsygithubappcompose repository demonstrates a production-ready approach to handling loading, error, and empty states in Compose screens using a single-source-of-truth pattern. By isolating state logic into a reusable wrapper component, every feature screen maintains consistent error handling and loading indicators without duplicating UI code.
Centralized State Management with GSYGeneralLoadState
The architecture centers on a single composable that acts as a gatekeeper for all screen-level state transitions.
The Wrapper Composable Implementation
Located at core/ui/src/main/java/com/shuyu/gsygithubappcompose/core/ui/components/GSYGeneralLoadState.kt, the GSYGeneralLoadState function accepts four parameters that completely define the screen's transient state:
isLoading: Boolean flag indicating active network or processing operationserror: Nullable string containing error messagesretry: Lambda function invoked when users tap the retry buttoncontent: The actual screen UI to render when neither loading nor error states are active
The implementation uses a Box with centered alignment to overlay states:
@Composable
fun GSYGeneralLoadState(
isLoading: Boolean,
error: String?,
retry: () -> Unit,
content: @Composable () -> Unit
) {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
when {
isLoading -> CircularProgressIndicator()
error != null -> Column(
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
text = error,
color = MaterialTheme.colorScheme.error
)
Button(onClick = retry) {
Text("Retry")
}
}
else -> content()
}
}
}
This design ensures that loading and error handling remain consistent across every screen while preserving the ability to customize the main content layout.
Screen-Level Integration Patterns
All feature screens wrap their UI inside GSYGeneralLoadState after being hosted by BaseScreen, which provides common ViewModel-level behaviors such as toast message handling.
TrendingScreen Implementation
The trending feature demonstrates conditional loading logic that distinguishes between initial page loads and subsequent pagination. In feature/trending/src/main/java/com/shuyu/gsygithubappcompose/feature/trending/TrendingScreen.kt, the loading state activates only when the first page is being fetched and the repository list is still empty:
BaseScreen(viewModel = viewModel) {
GSYGeneralLoadState(
isLoading = uiState.isPageLoading && uiState.repositories.isEmpty(),
error = uiState.error,
retry = { viewModel.refresh() }
) {
GSYPullRefresh(
isRefreshing = uiState.isRefreshing,
onRefresh = { viewModel.refresh() }
) {
// LazyColumn content displays when loaded
items(uiState.repositories) { repo ->
RepositoryItem(repo.toTrendingDisplayData())
}
}
}
}
If the request fails, the error UI with a retry button appears immediately. When loading succeeds, the pull-to-refresh list renders; an empty list simply results in no visible items.
SearchScreen and InfoScreen Variations
The search functionality in feature/search/src/main/java/com/shuyu/gsygithubappcompose/feature/search/SearchScreen.kt uses the same wrapper but checks multiple data sources:
GSYGeneralLoadState(
isLoading = isPageLoading && repositoryResults.isEmpty() && userResults.isEmpty(),
error = error,
retry = { searchViewModel.performSearch() }
) {
// Combined repository and user results list
}
Similarly, InfoScreen (user profile) handles single-object fetching in feature/info/src/main/java/com/shuyu/gsygithubappcompose/feature/info/InfoScreen.kt:
GSYGeneralLoadState(
isLoading = uiState.isPageLoading && uiState.user == null,
error = uiState.error,
retry = { viewModel.refresh() }
) {
// Profile content list
}
Handling Empty Data States
The empty state is handled implicitly rather than through explicit UI components. When the content lambda receives an empty list (e.g., uiState.repositories.isEmpty()), the composable simply draws nothing inside the LazyColumn or container.
This approach keeps the UI lightweight and allows each screen to decide whether to render a rich placeholder. The current implementation opts for a minimal blank view as the default empty state, though developers could easily extend the pattern by checking list emptiness within the content block or creating a dedicated GSYEmptyState composable.
Benefits of This Architecture
- Declarative state management: Each screen declares when it is loading or errored through simple boolean expressions, avoiding embedded UI logic throughout the layout hierarchy.
- Visual consistency:
GSYGeneralLoadStateensures identical spinner colors, error text styling, and retry button appearances across all features. - Compose-friendly composition: The wrapper integrates seamlessly with other high-order components like
GSYPullRefreshandLazyColumnwithout interfering with their internal state mechanics. - Extensibility: Future enhancements can modify the central wrapper or add explicit empty-state branches without refactoring individual screens.
Summary
GSYGeneralLoadStateincore/ui/src/main/java/com/shuyu/gsygithubappcompose/core/ui/components/GSYGeneralLoadState.ktprovides the centralized mechanism for rendering loading, error, and content states.- Screens wrap their content inside this composable after
BaseScreensetup, passing Boolean loading flags and nullable error strings derived from ViewModel state. - Loading states typically combine
isPageLoadingflags with emptiness checks to avoid showing spinners during pagination. - Error states display a retry button that invokes ViewModel refresh methods, centralizing error recovery logic.
- Empty states are implicitly handled by rendering blank content when data lists are empty, keeping the implementation lightweight.
Frequently Asked Questions
How does GSYGeneralLoadState differ from using LaunchedEffect for loading states?
GSYGeneralLoadState is a presentational wrapper that reacts to state changes through recompositions, while LaunchedEffect is used for side effects like triggering network requests. The composable receives isLoading as a parameter and declaratively chooses which UI to display, making the loading state a function of the UI layer rather than an imperative side effect.
Can I customize the error UI for specific screens while keeping the wrapper?
Yes. While the current implementation uses a standard error message and retry button, you could extend GSYGeneralLoadState to accept an optional @Composable errorContent: @Composable (String, () -> Unit) -> Unit parameter. This would allow individual screens to override the default error UI while maintaining the consistent loading state behavior.
Why is the empty state handled implicitly rather than explicitly in the wrapper?
The repository's design treats empty data as valid content—a list with zero items is still successfully loaded content. This avoids conflating "no data" with "error states" and gives screens flexibility: some may want to show "No results" illustrations while others prefer blank space. If explicit empty states become necessary, developers can add a conditional branch inside the content lambda or enhance GSYGeneralLoadState with an additional isEmpty parameter.
How does this pattern handle pull-to-refresh alongside loading states?
The architecture layers GSYGeneralLoadState outside GSYPullRefresh. The wrapper handles the initial page load (full-screen spinner), while the pull-refresh component manages subsequent refresh indicators within the content area. This separation prevents conflicting loading indicators and ensures users see appropriate feedback for initial loads versus content updates.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →