If user interfaces (UI) act as the canvas for creating digital experiences, Composable functions resemble skilled artists, forming and managing the aspects of this canvas, thereby facilitating dynamic and interactive user experiences. Beyond the canvas, a group of guiding principles and structures known as recompose scopes play a critical role. They behave like conductors for different sections of the UI, delivering precise updates where necessary in response to changing states or user interactions.
This exploration examines recompose scopes and their role in adaptable UIs and looks into the unique behavior of inline composable functions. By the end, you'll understand how they enhance composable functions for user-friendly UIs.
Recomposition scope
A recomposition scope in Compose is the smallest standalone unit of code that can be rerun when updates are needed. This implies that Compose can identify where changes in state or input occur and compute only that particular scope without affecting the entire composition.
Essentially, recomposition scopes help Compose update the user interface efficiently by monitoring changes in observed states within them. When these states change, the connected recomposition scope is invalidated, ensuring that only the relevant part of the code is recomposed before the next frame.
Before diving into the explanation, it's important to distinguish between two terms: composable recomposition, referring to the recomposition of composable functions, and scope recomposition. Think of the latter as a lambda function being passed into a composable that can recompose independently without triggering the recomposition of the rest. For instance, a 'Button scope' refers to the content lambda scope, which can recompose independently from the Button composable itself, as we'll see later.
In the provided example, there are three distinct composition scopes: CustomComposable scope, Surface scope and Button scope. Considering their hierarchical relationship, we observe that both the Button scope and Surface scope are nested within the CustomComposable scope.
However, in the context of recomposition, the Surface and Button scopes have the ability to recompose independently.
Compose can start a recomposition process for any of these scopes without needing to recompose the others.
You might wonder why we haven't discussed the Column Scope yet. We'll delve into it later.
Illustrating recompose scopes with an example
In this section, let's explore how the recompose scope works with a simple example.
val recompositionCount = mutableStateOf(0)
@Composable
fun CustomComposable() {
// This statement traces recomposition and is invoked each time it occurs
Log.d("Recomposition", "CustomComposable")
Button(onClick = { recompositionCount.value++ }) {
Log.d("Recomposition", "Button Scope ")
Text(text = "Recomposed ${recompositionCount.value} times.")
}
}In this example, we have created one Button with a Text composable within its content. When the Button is pressed, it updates the state that the Text composable reads. Here, we have two composition scopes: the CustomComposable Scope and the Button Scope. Both scopes can recompose independently.
As for the Text composable, it recomposes every time its input changes. But does it recompose independently? We'll examine this in the following lines.
Let's now run this example:
As depicted in the animation, when we tap the Button, only the Button scope will recompose because the Text composable recomposes as it changes its text parameter.
But why does the Button scope recompose, despite it being the Text composable that uses recompositionCount as input?
On the Java Virtual Machine (JVM) and Android Runtime (ART), the function's parameter is first pushed onto the call stack frame. Hence, the reason is that the calculation of the statement "Recomposed ${recompositionCount.value} times." occurs inside the Button scope before calling the Text function. To illustrate this, we can break down the Text function call as follows:
val newText = "Recomposed ${recompositionCount.value} times."
Text(text = newText)So it's the use of recompositionCount in the Button scope that triggers its recomposition, and the new value passed to the Text composable, newText, triggers its recomposition.
For the CustomComposable scope, since no state is directly consumed within it and the Button can recompose independently.
To observe composable functions recomposition, let's use Android Studio's Layout Inspector:
In the illustration above, the button is tapped once and we can see that the Button composable recomposed twice: once on press and once on release. However, this is not always the case, as the button's behavior depends on some complex internal states.
For instance, the Text composable recomposes once as its input changes just once. Similarly, The CustomComposable function doesn't recompose either because, on the whole, it consists of its content lambda (its scope), and as we've seen, its scope doesn't read any state.
Let's now release the Button's content scope and utilize the state within a Surface composable.
val recompositionCount = mutableStateOf(0)
@Composable
fun CustomComposable() {
Log.d("Recomposition", "CustomComposable Scope")
Surface {
Log.d("Recomposition", "Surface Scope")
Text(text = "Recomposed ${recompositionCount.value} times.")
}
Button(
onClick = { recompositionCount.value++ },
modifier = Modifier.padding(top = 20.dp)
) {
Log.d("Recomposition", "Button Scope ")
Text(text = "Tap to recompose")
}
}Now, the Button only serves to update the state and doesn't consume it within its scope.
Let's run it:
Initially, the Button scope is composed once for the first rendering. Afterward, only the Surface scope is recomposed. The CustomComposable scope also doesn't need to be recomposed because The Surface's scope is independent.
About the onClick Lambda:
The main function of the `onClick` lambda is to modify the underlying state; it doesn't observe or read any state. This state change, in turn, triggers recomposition in any composable functions that observe or use that state.
Inline composable functions and their parent scope impact
You might have also noticed the use of the modifier as a parameter for the Button.
modifier = Modifier.padding(top = 20.dp)This is used to ensure the Surface remains visible on the screen, as it would otherwise be hidden by the button. However, to arrange elements vertically, we typically use The Column composable. Let's remove the modifier parameter and The Surface composable, and use Column Composable to arrange the Text and the Button vertically
val recompositionCount = mutableStateOf(0)
@Composable
fun CustomComposable() {
Log.d("Recomposition", "CustomComposable Scope")
Column {
Log.d("Recomposition", "Column Scope")
Text(text = "Recomposed ${recompositionCount.value} times.")
Button(
onClick = { recompositionCount.value++ },
modifier = Modifier.padding(top = 20.dp)
) {
Log.d("Recomposition", "Button Scope ")
Text(text = "Tap to recompose")
}
}
}In the code snippet above, the state is now consumed by the Text composable, which is wrapped in the Column Scope. Accordingly, we would expect that only the Column scope would recompose when the Button is clicked.
Let's run it:
Oops! This isn't what we expected. The Column Scope doesn't recompose on its own, instead, it triggers the recomposition of the CustomComposable Scope as well. So what's happening here?
To understand this scenario, let's examine the source code of the Column function:
Upon examination, we discover that it is an inline function. Given that inline composable functions like Column, Row and Box have their code directly inserted into where they are called, these functions do not establish their separate recompose scopes.
Conclusion
In this exploration, we embarked on a journey into the realm of recompose scopes, composable functions, and the unique behavior of inline composable functions. Let's summarize the key points covered:
Recompose scopes are precision instruments, allowing Compose to update UI components efficiently by isolating recomposition.
Notably, inline functions like Column, Row, and Box are unique; they don't establish separate recompose scopes due to their inline nature.
Keep these insights in mind as you venture into the realm of modern UI development. They are the foundation upon which you can build user-friendly and efficient user interfaces.