Jetpack Compose UI Phases: Composition, Layout, and Drawing

Quick answer: Compose produces a frame in three main phases: composition decides what UI exists, layout measures and places it, and drawing renders it. Where your code reads state matters because a state change restarts the work from the phase where Compose observed that read.
This model is more useful than trying to memorize isolated performance tips. It explains why a changing padding value can trigger recomposition, why Modifier.offset { ... } can defer a read into layout, and why a Canvas color change can require only a redraw.
The official Jetpack Compose phases guide describes the normal flow as composition → layout → drawing. Compose can skip work that is not needed, but the correct first goal is a clear, state-driven UI—not forcing every update into a later phase.
The three phases at a glance
| Phase | Question it answers | Main work | A simple example |
|---|---|---|---|
| Composition | What should appear? | Runs composable functions and produces a UI tree | An if decides whether to show a progress indicator |
| Layout | How large is it and where does it go? | Measures and places layout nodes | A Row measures its children and places them side by side |
| Drawing | Which pixels should be rendered? | Draws backgrounds, text, shapes, images, and effects | A Canvas draws a colored circle |
Conceptually, this sequence can happen for each frame. In practice, Compose tracks state reads and does the minimum work it can safely do. A UI change does not automatically mean that every composable, every layout node, and every pixel is rebuilt.
Phase 1: composition decides what exists
During composition, Compose runs @Composable functions and creates a description of the UI tree. Kotlin control flow belongs here: if, when, loops, and calls to other composables all determine what the screen contains.
import androidx.compose.foundation.layout.Column
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
@Composable
fun DownloadStatus() {
var isDownloading by remember { mutableStateOf(false) }
Column {
Text(text = "Report")
if (isDownloading) {
Text(text = "Downloading…")
}
}
}The read of isDownloading happens in composition because the value decides whether the second Text exists. When it changes, Compose needs to revisit that decision. This is recomposition: Compose re-runs the relevant composable functions with current inputs.
Composable functions should be fast and free of side effects. They may run again, be skipped, or run in an order that differs from how you imagine the screen being built. The Compose mental model explains why work such as a network call, a shared-preference write, or a ViewModel mutation should not happen directly in a composable body.
For a practical explanation of state ownership and events, read State Hoisting in Jetpack Compose.
Phase 2: layout measures and places
The layout phase takes the UI tree from composition and determines each element’s size and position. It has two closely related steps:
- Measurement: a parent asks its children how much space they need, then determines its own size from the available constraints and those measurements.
- Placement: the parent assigns each child an x/y position within its own bounds.
For a simple Row, Compose measures the children, uses those measurements to determine the Row size, then places the children horizontally. The detailed layout basics guide describes this single-pass measurement model.
The measurement and placement steps have separate restart scopes. That matters when a change only moves an element rather than changing its size. Compose can sometimes repeat placement without repeating the matching measurement work, although a placement change can still affect other layout work in the tree.
A state read during composition
This code creates a modifier while the composable runs, so padding is read in composition:
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
@Composable
fun CompositionReadExample() {
var padding by remember { mutableStateOf(8.dp) }
Text(
text = "Hello",
modifier = Modifier.padding(padding),
)
}If padding changes, Compose schedules recomposition for the function that read it. Layout and drawing may follow if the new composition changes size, position, or visuals.
A state read during placement
The lambda overload of Modifier.offset defers its state read to placement:
import androidx.compose.foundation.layout.offset
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.roundToPx
@Composable
fun PlacementReadExample() {
var offsetX by remember { mutableStateOf(8.dp) }
Text(
text = "Hello",
modifier = Modifier.offset {
IntOffset(x = offsetX.roundToPx(), y = 0)
},
)
}When offsetX changes, Compose restarts layout work from the placement read rather than treating the value as a composition input. This is not a reason to replace every ordinary modifier with a lambda. It is useful when a frequently changing value genuinely affects placement and you have measured a problem. See Jetpack Compose Modifier: A Practical Guide for the broader behavior of modifier chains.
Phase 3: drawing renders pixels
After layout assigns sizes and positions, drawing renders the UI. A node can draw its background before its children, then each child draws in turn. Drawing covers familiar UI work such as text, images, shapes, clipping, shadows, and custom Canvas content.
A state value read inside drawing code can be observed in the draw phase:
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.layout.size
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
@Composable
fun DrawReadExample() {
var color by remember { mutableStateOf(Color.Red) }
Canvas(modifier = Modifier.size(80.dp)) {
drawCircle(color = color)
}
}Changing color schedules drawing for the reader. Because the example does not change which UI exists, its size, or its position, Compose can avoid repeating composition and layout for that change. The performance phases guide uses this separation to explain why later state reads can reduce unnecessary work.
State reads determine the restart point
Compose tracks snapshot-state reads by phase. This table is a useful debugging model:
| Where state is read | When that state changes, Compose starts with | What may happen afterward |
|---|---|---|
| In a composable function or its lambda | Composition | Layout and drawing if needed |
| During measurement | Layout | Drawing if needed |
During placement, such as offset { } | Layout placement | Drawing if needed; measurement may be skipped for that restart scope |
In drawing code, such as Canvas or drawBehind | Drawing | Only drawing for that reader |
This behavior is why it is important to distinguish recomposition from every UI update. Recomposition is the composition-phase work; the layout and drawing phases have their own work and can restart independently.
Use the phases to diagnose, not guess
If an animation stutters or scrolling becomes janky, start with an observable symptom and a measurement. Do not assume that composition is automatically wrong just because it occurs.
- Reproduce the user flow in a release-like build where possible.
- Use Android Studio’s Layout Inspector to inspect the hierarchy and recomposition counts.
- Use composition tracing or a system trace to form a hypothesis about which work occurs too often.
- Identify the state read that causes the repeated work.
- Make one targeted change and measure the same flow again.
Android’s Compose performance tooling guide recommends Layout Inspector for recomposition counts and composition tracing for investigating a performance issue. Recomposition itself is not a bug; unexpected or expensive work in a critical interaction is the problem to investigate.
Common mistakes when learning the phases
“Every state change redraws the whole app”
No. Compose tracks reads and can skip composition, layout, or drawing work whose inputs do not require it. The scope and result depend on the state read and the UI tree.
“Avoiding recomposition is always the goal”
No. Recomposition is the normal way declarative UI reacts to state. Prefer clear state flow first. Optimize only after observing a real cost, and make sure an optimization does not hide a needed UI update.
“A later state read is always better”
No. The phase must match the UI behavior. A value that decides whether a component exists belongs in composition. A value that changes size must participate in layout. Moving a read only makes sense when it preserves the intended visual result.
“A draw-only change has no cost”
It can still be expensive if the drawing work is complex or runs every animation frame. The benefit is that Compose may avoid unrelated composition and layout work, not that rendering becomes free.
What to learn next
The phases explain the machinery beneath everyday Compose code. Next, learn how your layout choices translate into constraints and positions with Row vs Column vs Box, then investigate actual performance behavior with the recomposition debugging guide.
FAQ
Does Compose run all three phases for every state change?
Not necessarily. Compose can skip work when the changed state and its read location do not require a phase to run again. A draw-phase state read, for example, can restart drawing without requiring composition or layout for that reader.
What is the difference between recomposition and layout?
Recomposition runs composable functions to decide what UI exists. Layout measures and places the resulting UI tree. They are separate phases, even though a recomposition can lead to layout work.
Should I use lambda modifiers everywhere?
No. Use the clearest normal modifier first. Lambda modifiers can defer a frequently changing state read into layout or drawing, but they are a targeted tool to apply after you understand the behavior and have measured a relevant cost.