Declarative UI, Composition, Recomposition and State
Part 1: Declarative UI
1. Declarative vs Imperative UI
Imperative UI
In the traditional View system, we manually find and update individual views.
progressBar.visibility = View.VISIBLE
errorText.visibility = View.GONE
productList.visibility = View.GONEWe tell the UI how to change step by step.
Declarative UI
In Compose, we describe what the UI should look like for the current state.
when {
state.isLoading -> CircularProgressIndicator()
state.error != null -> Text(state.error)
else -> ProductList(state.products)
}When the state changes, Compose updates the required UI automatically.
flowchart LR
S["Current State"] --> C["Composable"]
C --> UI["Required UI"]| Imperative UI | Declarative UI |
|---|---|
| Manually modifies views | Describes UI from state |
Uses findViewById() | Calls composable functions |
| Updates each view separately | State change updates UI |
| Focuses on how to update | Focuses on what to display |
Compose uses the declarative approach: UI is described from data, and Compose applies only the necessary changes. Android: Thinking in Compose
---
2. Composable Functions
A composable function is a Kotlin function that describes a part of the UI.
It uses the @Composable annotation.
@Composable
fun Greeting(name: String) {
Text(text = "Hello, $name")
}Composable functions can call other composable functions:
@Composable
fun ProfileScreen(name: String) {
Column {
Text("Profile")
Greeting(name)
Button(onClick = {}) {
Text("Continue")
}
}
}Important properties
A composable should be:
* Fast
* Reusable
* Free from unnecessary side effects
* Based on input parameters and state
A composable can run many times, be skipped, or run in a different order. Therefore, avoid API calls, database writes or shared-variable changes directly inside its body.
---
3. UI as a Function of State
The most important Compose formula is:
UI = Function(State)flowchart LR
S1["Loading State"] --> F["Screen Composable"]
S2["Success State"] --> F
S3["Error State"] --> F
F --> UI["Correct UI"]Example state:
data class ProductUiState(
val isLoading: Boolean = false,
val products: List<Product> = emptyList(),
val error: String? = null
)Example UI:
@Composable
fun ProductScreen(state: ProductUiState) {
when {
state.isLoading -> CircularProgressIndicator()
state.error != null -> Text(state.error)
else -> ProductList(state.products)
}
}The same composable produces different UI for different states.
---
4. Composition Tree
Composable functions call other composables and create a tree-like structure called the composition tree.
@Composable
fun LoginScreen() {
Column {
Text("Login")
EmailField()
PasswordField()
LoginButton()
}
}flowchart TD
L["LoginScreen"] --> C["Column"]
C --> T["Text"]
C --> E["EmailField"]
C --> P["PasswordField"]
C --> B["LoginButton"]Compose remembers the position and identity of composables in this tree.
When state changes, Compose checks which part of the tree read that state and updates only the affected part where possible.
---
5. Data Flow
Compose normally follows one-way data flow:
* Data flows downward.
* Events flow upward.
flowchart TD
P["Parent"] -->|State| C["Child"]
C -->|Event| PExample:
@Composable
fun Counter(
count: Int,
onIncrement: () -> Unit
) {
Button(onClick = onIncrement) {
Text("Count: $count")
}
}* count flows from parent to child.
* onIncrement sends the click event to the parent.
This keeps the UI predictable.
---
6. State Hoisting
State hoisting means moving state from a child composable to its parent.
Stateful composable
@Composable
fun Counter() {
var count by remember { mutableIntStateOf(0) }
Button(onClick = { count++ }) {
Text("Count: $count")
}
}The composable owns and changes its state.
Stateless composable
@Composable
fun Counter(
count: Int,
onIncrement: () -> Unit
) {
Button(onClick = onIncrement) {
Text("Count: $count")
}
}Parent:
@Composable
fun CounterScreen() {
var count by remember { mutableIntStateOf(0) }
Counter(
count = count,
count++ }
)
}flowchart LR
P["Parent owns count"] -->|count| C["Counter"]
C -->|onIncrement| PHoisted composables are easier to reuse, preview and test. State should normally be hoisted to the lowest common parent that needs to read or change it. Android: Where to hoist state
---
Part 2: Composition, Recomposition and State
7. Composition
Composition is the first execution of composable functions used to create the UI.
flowchart LR
C["Composable called first time"] --> CT["Composition created"]
CT --> UI["UI displayed"]During composition, Compose:
1. Executes composable functions.
2. Records the UI structure.
3. Tracks State values read by each composable.
4. Stores required information in the composition.
---
8. Recomposition
Recomposition means executing affected composable functions again when their observed inputs change.
flowchart LR
E["User Action"] --> S["State changes"]
S --> R["Recomposition"]
R --> UI["UI updated"]Example:
@Composable
fun Counter() {
var count by remember { mutableIntStateOf(0) }
Button(onClick = { count++ }) {
Text("Count: $count")
}
}When count++ runs:
1. The State value changes.
2. Compose knows that Text read count.
3. The affected scope becomes invalid.
4. Compose schedules recomposition.
5. The text displays the new value.
Compose can skip composables whose inputs have not changed. Android: Thinking in Compose
---
9. remember
Local variables normally disappear when a composable runs again.
Without remember
var count = 0The value is recreated during recomposition, so it does not work as observable Compose state.
With remember
var count by remember {
mutableIntStateOf(0)
}remember stores the value inside the current composition.
It survives:
* Recomposition
It does not survive:
* Configuration change
* Activity recreation
* Process death
---
10. rememberSaveable
rememberSaveable retains small UI state across recomposition and recreation.
var name by rememberSaveable {
mutableStateOf("")
}| API | Recomposition | Screen rotation | Process recreation* |
|---|---|---|---|
| Normal variable | No | No | No |
remember | Yes | No | No |
rememberSaveable | Yes | Yes | Usually yes |
*Provided the value can be saved and Android restores saved instance state.
rememberSaveable uses Android’s saved-state mechanism. Use it for small values such as text, selections, IDs and expanded status—not large lists or bitmaps. Android: State and Compose, Android: Save UI state
---
11. Mutable State
mutableStateOf() creates an observable State value.
var name by remember {
mutableStateOf("")
}When Compose reads name, it records the read. When name changes, the reader may be recomposed.
These two forms are equivalent:
val name = remember { mutableStateOf("") }
Text(name.value)
name.value = "Mahesh"var name by remember { mutableStateOf("") }
Text(name)
name = "Mahesh"For primitives, specialized APIs can avoid boxing:var count by remember { mutableIntStateOf(0) }
var enabled by remember { mutableStateOf(false) }---
12. Compose Snapshot System
Compose State is managed by a snapshot system.
A snapshot is like a consistent view of State at a particular moment.
flowchart TD
R["Composable reads State"] --> T["Compose tracks the read"]
W["State value changes"] --> N["Readers are notified"]
N --> RC["Affected work reruns"]In simple terms:
1. A composable reads a State value.
2. Compose remembers where it was read.
3. The State changes.
4. Compose invalidates the affected reader.
5. The required composition, layout or drawing work runs again.
This tracking is why changing a normal variable does not update Compose, but changing MutableState does. Android: Compose phases
---
13. What Triggers Recomposition?
Common recomposition triggers include:
* A read MutableState value changes
* A composable parameter changes
* Collected StateFlow or LiveData emits new data
* A Composition Local value changes
* A parent recomposes and the child cannot be skipped
Example:
@Composable
fun Welcome(name: String) {
Text("Welcome $name")
}If `name` changes, `Welcome()` may recompose.What does not trigger recomposition?
var count = 0
count++A normal variable is not observable State, so Compose is not notified.
---
14. Stability
Stability helps Compose decide whether a composable can be skipped.
A value is conceptually stable when Compose can reliably determine whether it changed.
Common stable values
* String
* Int
* Boolean
* Immutable data with stable properties
* Types explicitly designed as stable
Example:
@Immutable
data class UserUiModel(
val id: String,
val name: String
)Problematic mutable object
data class User(
var name: String
)If `name` changes internally without observable State, Compose may not receive a notification.Prefer immutable models:
data class User(
val name: String
)
user = user.copy(name = "Amit")Important point
Stability does not mean the object never changes. It means Compose can reliably know when a change should affect the UI.
---
15. derivedStateOf
derivedStateOf creates State calculated from other State values.
Use it when source State changes frequently but the UI only needs to update when the calculated result changes.
val listState = rememberLazyListState()
val showScrollToTop by remember {
derivedStateOf {
listState.firstVisibleItemIndex > 0
}
}flowchart LR
LS["List position changes often"] --> D["derivedStateOf"]
D --> B["Show button: true or false"]
B --> UI["UI updates only when result changes"]Without derived State, scrolling could cause unnecessary recompositions for every position change.
Do not use it for simple calculations
Unnecessary:
val fullName by remember {
derivedStateOf { "$firstName $lastName" }
}For a cheap calculation that should update whenever its inputs change, use:
val fullName = "$firstName $lastName"---
Complete Internal Flow
sequenceDiagram
actor U as User
participant UI as Composable
participant ST as Mutable State
participant CR as Compose Runtime
U->>UI: Click button
UI->>ST: Change value
ST->>CR: Notify state change
CR->>CR: Find readers
CR->>UI: Recompose affected scope
UI-->>U: Display updated UIIn easy language
1. Composable reads State.
2. Compose tracks that read.
3. User performs an action.
4. State value changes.
5. Compose identifies affected UI.
6. Affected composables recompose.
7. Unchanged work may be skipped.
8. The updated UI is displayed.
---
Common Mistakes
1. Using a normal variable as State
var count = 0 // UI will not observe changesUse:
var count by remember { mutableIntStateOf(0) }2. Forgetting remember
var count by mutableIntStateOf(0)This creates State again whenever the composable runs.
3. Expecting remember to survive rotation
Use rememberSaveable for small restorable UI state.
4. Keeping large data in rememberSaveable
Do not store bitmaps, large lists or complex screen data in its saved-state Bundle.
5. Performing API calls directly in the composable body
@Composable
fun Screen() {
loadProducts() // May run during every recomposition
}Composable bodies can execute repeatedly. Use an appropriate effect or call through an event.
6. Mutating a normal collection
items.add(product)Compose may not observe the internal mutation. Prefer observable State or replace it with a new immutable list.
7. Hoisting every small state
Keep state local when no other composable needs to control it. Hoist it when sharing, testing or external control is required.
---
Interview Questions and Answers
Q1. What is declarative UI?
Declarative UI means describing what the screen should display for the current State. When State changes, Compose updates the required UI automatically.
Q2. What is a composable function?
It is a Kotlin function marked with @Composable that describes a part of the UI and can call other composable functions.
Q3. What is composition?
Composition is the first execution of composables that creates and records the UI structure.
Q4. What is recomposition?
Recomposition is the re-execution of affected composables when their observed inputs change.
Q5. Does Compose redraw the complete screen?
Usually no. Compose tracks State reads and tries to recompose only affected scopes while skipping unchanged composables.
Q6. What is the difference between remember and rememberSaveable?
remember survives recomposition. rememberSaveable can additionally restore supported small UI values after Activity or process recreation.
Q7. What is state hoisting?
State hoisting means moving State to a caller and passing the value down with an event callback.
Q8. What is the Compose snapshot system?
It tracks State reads and writes using consistent snapshots. When observable State changes, Compose schedules the affected work again.
Q9. What is stability?
Stability tells Compose whether it can reliably identify changes to a value and safely skip unnecessary recomposition.
Q10. When should we use derivedStateOf?
Use it when State changes frequently but the UI depends on a calculated result that changes less frequently.
---
Final Interview Answer
> Jetpack Compose uses declarative UI, where composable functions describe the UI from State. During the first execution, Compose creates a composition and records the composition tree and State reads. When observed State changes, Compose invalidates the affected scopes and performs recomposition while skipping unchanged work when possible. remember retains a value across recomposition, while rememberSaveable can restore supported small UI state after recreation. State hoisting moves State to a common owner, with data flowing down and events flowing up. Compose’s snapshot and stability systems help track State changes and avoid unnecessary work.