Composition is the central process in Jetpack Compose, during which a live UI tree displayed on the screen is built from descriptive Composable functions. Unlike the Android View system, where layouts were loaded from XML and converted into immutable objects, Composition works as a dynamic system: functions execute, create slots in memory, form a node hierarchy, and bind it to state. According to Google Android Developers, 2026, understanding Composition is critical for optimizing Compose application performance.
Key Takeaways
Composition is the process of executing Composable functions, resulting in an internal representation of the user interface as a tree of nodes. Each node in this tree corresponds either to a built-in component (Text, Button, Image) or to a call to a user-defined Composable function. Composition does not directly create Android View objects — it builds an abstract description that is then processed by the Layout and Drawing phases.
The key feature of Composition is its restartability. Each Composable function within the composition can be restarted at any time if its input parameters or the state objects it reads have changed. The system does not restart the entire tree — only those functions that actually depend on the changed data.
Technically, Composition is managed through Composer — an internal engine that the Kotlin compiler embeds into every Composable function. Composer writes information about which functions were called, with what parameters, and in what order into slots (position groups). On subsequent calls, Composer compares the new data with the stored data and decides whether to restart.
The process of building the UI tree begins with calling the setContent method inside an Activity or Fragment. This method creates the initial Composition and starts executing the root Composable function. Then each nested Composable function adds its nodes to the tree, forming a hierarchy: Row contains Text and Button, Column contains Image and Card, and so on.
Each tree node receives a unique position key, based on its position in the source code. This key is used to identify the node during subsequent executions. The position key is the reason why the order of calling Composable functions should not depend on conditions: if in one run A -> B is called, and in the next B -> A, Compose will not be able to match old and new nodes.
@Composable
fun AppScreen() {
Column { // Column node (position 1)
HeaderSection() // HeaderSection node (position 2)
ContentSection() // ContentSection node (position 3)
FooterSection() // FooterSection node (position 4)
}
}
@Composable
fun HeaderSection() {
Row { // Row node (position 2.1)
Text("Title") // Text node (position 2.2)
Icon(...) // Icon node (position 2.3)
}
}
In this example, each call receives a position based on the order in code. Column (position 1) contains three child nodes (positions 2, 3, 4). HeaderSection adds two more child nodes (2.1, 2.2, 2.3). If in the next recomposition ContentSection is called before HeaderSection, Composer will not be able to correctly match nodes — hence the rule: the order of Composable function calls must be stable.
State in Composition is managed through objects of type State<T>. When a Composable function reads a value from State through a delegated property (by), it registers a dependency on that State. When the value changes, all functions that read this State are marked for restart in the next composition phase.
The dependency registration mechanism is called the snapshot system. Each time State changes, a snapshot records all changes and notifies Composer which functions depend on that State. It is important to understand: reading State inside non-Composable code (e.g., in an onClick lambda) does not register a dependency — only reading inside a Composable function or in lambdas executed within the composition context.
The snapshot system works transactionally: multiple State changes within a single event are combined into one transaction, preventing multiple recompositions. This is especially important when handling gestures: one movement changes several State objects, but Compose performs only one recomposition.
@Composable
fun StateExample() {
var text by remember { mutableStateOf("Hello") }
var isVisible by remember { mutableStateOf(true) }
Column {
Text(text) // registers dependency on text
if (isVisible) { // registers dependency on isVisible
TextField(value = text, onValueChange = { text = it })
}
Button(onClick = { isVisible = !isVisible }) {
Text(if (isVisible) "Hide" else "Show")
}
}
}
Changing text triggers recomposition of only Column, Text, and TextField. Column, Button, and the isVisible condition remain unchanged. This isolation of recomposition is a key advantage of Compose over systems that redraw the entire screen. Each Composable function tracks only those State objects it directly reads.
Composition and Recomposition are two different modes of executing Composable functions. Composition occurs once when the screen is created: the system executes all Composable functions with initial values and builds the initial UI tree. Recomposition occurs multiple times when data changes: the system restarts only those functions that depend on the changed state.
Mode Composition activates all tree nodes, allocates slots for each function, and registers all descendants. Recomposition works selectively: Compose compares new and old parameter values for each function, and if they have not changed — the function is not executed (skipping).
Composition and Recomposition differ in cost. First Composition is more expensive because it requires full tree building and slot allocation. Recomposition is cheaper, especially if most functions are stable — their parameters are compared by equals, and Compose skips their invocation. For maximum performance, you should aim for most recompositions to affect as few functions as possible.
| Characteristic | Composition | Recomposition |
|---|---|---|
| When it occurs | Once, on first display | Multiple times, on data change |
| Scope | Entire tree | Only changed functions |
| Parameter comparison | Not performed | Performed for skipping |
| Slot creation | Yes, all slots created | Only for new nodes |
CompositionLocal is a mechanism for implicitly passing data through the composition tree. It solves the problem when a parameter needs to be passed through dozens of nested Composable functions that do not use it directly. Instead of an explicit parameter chain, data is set at the top level and read in any nested function through CompositionLocal.current.
The MaterialTheme is the most well-known example of CompositionLocal. All Compose components read colors, typography, and shapes through MaterialTheme.colorScheme, MaterialTheme.typography, MaterialTheme.shapes, without receiving them through parameters. Developers can create their own CompositionLocal for data such as the current user, localization settings, or screen configuration.
An important limitation: CompositionLocal should not be used for frequently changing data (scroll position, text in an input field). A component that reads CompositionLocal restarts every time the value changes, so for dynamic data it is better to use explicit parameters or State. CompositionLocal is optimal for configuration data that changes rarely or not at all.
val LocalUser = compositionLocalOf<User?> { null }
@Composable
fun AppRoot(user: User, content: @Composable () -> Unit) {
CompositionLocalProvider(LocalUser.provides(user)) {
content()
}
}
@Composable
fun UserAvatar() {
val user = LocalUser.current // reading without explicit parameter
AsyncImage(model = user?.avatarUrl, contentDescription = "Avatar")
}
CompositionLocalProvider creates a scope within which LocalUser.current returns the specified value. UserAvatar reads the user without explicitly passing the parameter through intermediate functions. This is especially valuable in deep hierarchies where data is only needed in a few leaf nodes.
Frequently Asked Questions
Changing State during Composition schedules a new recomposition, which will execute after the current one completes. No infinite loop occurs: Compose guarantees that each recomposition is performed in a separate snapshot system transaction.
On modern devices, Composition of a screen with 50–100 Composable functions takes 1–5 ms. Google recommends staying within 16 ms for a 60fps frame. If Composition exceeds this limit, use LazyColumn or break the screen into smaller functions.
Direct manual start of Composition is not possible — it is managed by Composer automatically. However, you can force a recomposition by changing State or calling invalidate() on the root composable if you have access to CompositionContext.
The View hierarchy is an immutable tree of Java objects that is created once. Composition is a virtual tree that is rebuilt each time data changes. View stores its state in instance variables, Composition — in slots bound to the function call position.
If a Composable function is no longer called (e.g., an if condition becomes false), Composition removes its node and triggers DisposableEffect cleanup. When it reappears (if becomes true again), a new node is created — the old one is not restored.
Summary
We will develop a mobile application turnkey
IT Sectr creates iOS and Android applications for startups and businesses since 2017. We will advise you and propose the best solution.
Read also