Declarative UI in Jetpack Compose Explained

Declarative UI means describing what the screen should look like for the current state instead of imperatively changing individual widgets. In Compose, you call the same composable with new state; Compose determines the necessary UI updates.

Android’s Thinking in Compose guide explains that Compose avoids manually mutating a View tree. State flows down into composables and events flow upward to the owner that changes state.

Imperative versus declarative

// Imperative View-style thinking
button.text = if (isSaved) "Saved" else "Save"

// Declarative Compose thinking
@Composable
fun SaveButton(isSaved: Boolean, onClick: () -> Unit) {
    Button(onClick = onClick) {
        Text(if (isSaved) "Saved" else "Save")
    }
}

The button does not own the saving decision. It renders isSaved and forwards the click. A ViewModel updates UI state, then Compose calls the composable again with the new argument.

State down, events up

data class ProfileUiState(val isFollowing: Boolean = false)

@Composable
fun ProfileScreen(state: ProfileUiState, onToggleFollow: () -> Unit) {
    FollowButton(
        isFollowing = state.isFollowing,
        onClick = onToggleFollow
    )
}

This unidirectional flow makes state easier to test and prevents different UI nodes from drifting out of sync. For full state ownership patterns, see state hoisting.

Recomposition is not rebuilding everything

When observed state changes, Compose can re-execute affected composables and apply only required updates. Write small composables, keep data immutable, and avoid side effects during rendering. The Compose UI phases guide explains composition, layout, and drawing in detail.

Common mistakes

  • Keeping application state inside a leaf composable instead of its state owner.
  • Calling network or database work directly while rendering.
  • Manually trying to mutate a previous UI representation.
  • Treating recomposition as a reason to avoid clear, state-driven code.

FAQ

Does declarative mean no state?

No. It means state is the input to UI, rather than something each widget is manually mutated to reflect.

Does Compose redraw the entire app on every change?

No. Compose tracks state reads and updates the affected UI work as needed.