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.GONE

We 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 UIDeclarative UI
Manually modifies viewsDescribes UI from state
Uses findViewById()Calls composable functions
Updates each view separatelyState change updates UI
Focuses on how to updateFocuses 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| P

Example:

@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| P

Hoisted 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 = 0

The 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("")
}
APIRecompositionScreen rotationProcess recreation*
Normal variableNoNoNo
rememberYesNoNo
rememberSaveableYesYesUsually 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 UI

In 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 changes

Use:

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.