Skip to content
AB

Keeping Compose Screens Free of Network and Storage Work

Let composables describe UI while ViewModels and repositories own effects, retries, and persistence.

Ashish BhakhandPublished

Composable functions can run many times. Starting a network call because a composable executed couples business work to rendering behavior.

Use the screen as a state and event boundary:

Code
@Composable
fun ReportsScreen(
    state: ReportsState,
    onIntent: (ReportsIntent) -> Unit,
) {
    ReportsContent(
        reports = state.reports,
        loading = state.loading,
        onRefresh = { onIntent(ReportsIntent.Refresh) },
    )
}

A route-level composable can obtain the ViewModel and collect state. Lower components stay previewable and testable with plain values.

LaunchedEffect is appropriate for UI lifecycle coordination, such as sending an initial load intent keyed to a stable identifier. It should not become a substitute for repository architecture.

File access, Room queries, Health Connect reads, and provider calls live behind ViewModel dependencies. The ViewModel decides whether repeated intent is ignored, cancelled, or restarted.

Previews now need no dependency graph. UI tests can render states directly: loading, empty, populated, denied permission, and failed refresh.

Compose is declarative when rendering describes what the user sees. Effects remain explicit inputs to the state machine rather than side effects of drawing it.

← Back to all writing