Blog Infos
Author
Published
Topics
Published

Photo by USGS on Unsplash

 

State

State is the pivot in the declarative world. Recomposition happens when there is a modification in the state. There are two methods for altering a state value, which will consequently trigger recomposition:

  • Update the inputs of the function.
  • Update the state variable, typically done through mutableStateOf.

Let’s break down how state functions within a composable. Consider the following example:

@Composable
private fun SimpleTextField() {
var text = ""
TextField(value = text, onValueChange = {
text = it
})
}
view raw gistfile1.kt hosted with ❤ by GitHub

This composable won’t work because text is just a local variable and its value won’t persist during recomposition.

So, how can we resolve this?

@Composable
private fun SimpleTextField() {
var text by remember { mutableStateOf("") }
TextField(value = text, onValueChange = {
text = it
})
}
view raw gistfile1.kt hosted with ❤ by GitHub

In this adjustment, we’ve performed two actions:

  • By assigning text with mutableStateOf(), we declare that the text is now a state, to be precise, a mutable state.
  • By inserting remember, we ensure that the value of this text is retained across recompositions.

With these changes, the TextField should now function properly.

Recomposition

Compose starts identifying the scope of changes as soon as there are updates with inputs or states. To illustrate, let’s check this example:

@Composable
fun Sample() {
var button1 by remember { mutableStateOf("button 1") }
var button2 by remember { mutableStateOf("button 2") }
Column(horizontalAlignment = Alignment.CenterHorizontally) {
// Recomposes when `button1` changes, but not when button2 changes
Button(onClick = { button1 += "1" }) { Text(text = button1) }
Spacer(modifier = Modifier.height(8.dp))
// Recomposes when `button2` changes, but not when button1 changes
Button(onClick = { button2 += "2" }) { Text(text = button2) }
}
}
view raw jc-sample.kt hosted with ❤ by GitHub

The best way to debug how many times a composable is being recomposed is to use Layout Inspector (Android Studio > Tools > Layout inspector).

In the demonstration, a blue rectangle bordering an element indicates the recomposition scope.

This behaves as expected, composables recompose independently when reading updated states.

Composition works in the following way:

  1. Initial State: The entire composable gets composed with the initial values.
  2. User clicks on button1: Its state variable updates, triggering a recomposition within the first button’s scope.
  3. User clicks on button2: Its state variable updates separately from button1, leading to recomposition within the second button’s scope.
Understanding Recomposition Scope

Recomposition ensures that the UI is always updated with the latest states. However, repeatedly rendering the entire screen can be costly and impact app performance. To optimize this process, Compose tries to skip redundant renders whenever possible.

Let’s try to understand it by adding another Row to our composable:

@Composable
fun Sample() {
var button1 by remember { mutableStateOf("button 1") }
var button2 by remember { mutableStateOf("button 2") }
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Button(onClick = { button1 += "1" }) { Text(text = button1) }
Spacer(modifier = Modifier.height(8.dp))
Button(onClick = { button2 += "2" }) { Text(text = button2) }
// New row added here, expected to recompose when button1 changes.
Row {
Text(text = button1)
}
}
}

Job Offers

Job Offers

There are currently no vacancies.

OUR VIDEO RECOMMENDATION

, ,

Cutting-Edge-to-Edge in Android 15: Using Previews and Testing in Jetpack Compose to Manage Insets.

With the advent of Android 15, edge-to-edge design has become the default configuration. Consequently, applications must be capable of accommodating window insets, including the system status bar and navigation bar, as well as supporting drawing…
Watch Video

Cutting-Edge-to-Edge in Android 15: Using Previews and Testing in Jetpack Compose to Manage Insets.

Timo Drick
Lead Android developer
Seven Principles Mobility GmbH

Cutting-Edge-to-Edge in Android 15: Using Previews and Testing in Jetpack Compose to Manage Insets.

Timo Drick
Lead Android develop ...
Seven Principles Mob ...

Cutting-Edge-to-Edge in Android 15: Using Previews and Testing in Jetpack Compose to Manage Insets.

Timo Drick
Lead Android developer
Seven Principles Mobility ...

Jobs

Reading state in row trigger recompose to entire composable

 

The unexpected behavior occurs when reading the state inside the Row scope, causing the entire composable to recompose and this is not desired, we only want recomposition when changes occur in Button1 and the newly added Row.

The reason for this behavior is because of the implementation of Row in Compose. It’s actually an inline function — a very interesting feature of Kotlin. Let’s deep dive into it.

Understanding Inline Functions in Kotlin
Example 1
fun main() {
print("foo ")
bar()
// prints: foo bar
}
inline fun bar() {
print("bar")
}

Let’s decompile this into bytecode and see what we’ll get.

// simplified.
fun main() {
print("foo ")
print("bar");
// prints: foo bar
}

(To see the decompiled code in Android Studio, open Tools > Kotlin > Show Kotlin Bytecode > Decompile)

 

So calling function bar() will always create the copied code to the call site of themain() function.

Example 2

Kotlin supports creating higher-order functions that take functions as parameters. Let’s create a function and pass lambda types () -> Unit to it:

fun main() {
print("foo ")
bar {
print("bar");
}
// prints: foo bar
}
inline fun bar(invoke: () -> Unit) {
invoke()
}

which ends up resulting same bytecode as the previous example.

fun main() {
print("foo ")
print("bar");
// prints: foo bar
}

When calling an inline function, Kotlin does not create any instance of Function behind the scene. Instead, it directly passes the function’s code into the call site where it is invoked. By doing this, Kotlin can reduce the number of redundant objects/memory allocations.

Using inline without lambda is not recommended

 

In fact, Kotlin recommends not to use inline functions without a lambda type because it may not provide any benefits compared to regular functions. In contrast, by passing lambda types as parameters, we can leverage their nature to treat them as functions without introducing additional scopes.

Using Lambda Types in Compose

Returning to our previous discussion, the unexpected behavior occurs when we add a new element and read the state hence triggering a recomposition to the nearest scope.

This behavior is due to the implementation of functions like RowColumnBox, and Layout in Compose. These functions are marked as inline, which means they don’t create separate scopes but pass their call block directly to the call site.

@Composable
inline fun Row(
modifier: Modifier = Modifier,
horizontalArrangement: Arrangement.Horizontal = Arrangement.Start,
verticalAlignment: Alignment.Vertical = Alignment.Top,
content: @Composable RowScope.() -> Unit
) {
// implementation details
}
view raw row.kt hosted with ❤ by GitHub

To address this issue and ensure that the sample composable recomposes only for the intended views, we can create a new function with a lambda type and call it as follows:

@Composable
fun MyRow(content: @Composable () -> Unit) {
content()
}
@Composable
fun Sample() {
var button1 by remember { mutableStateOf("button 1") }
Column(horizontalAlignment = Alignment.CenterHorizontally) {
// other composables
MyRow {
Text(text = button1)
}
}
}

By introducing the non-inline function MyRow with a lambda parameter, we can control the recomposition scope. The content passed to MyRow will be treated as one level below the call site, preventing unnecessary recompositions in the outer scope.

Here is our final result ❤️

Conclusion

We often don’t need to worry too much about handling recomposition and redundant renders in Compose. It will try to skip as much as possible. However, taking advances in lambda types will help us a lot in improving performance or ensuring every portion of our UI is always validated with the committed states.

One noteworthy example is when dealing with resource-intensive operations, such as loading large bitmaps or animating views during scroll states. Performing such operations directly at the composition level can be expensive and may lead to UI glitches or janky animations.

To address this, we can perform these resource-intensive operations in the background, outside the composition. By using a lambda type, such as providers: () -> Unit, we can pass the result of these operations to the UI at specific times or intervals.

Further Reading:

I highly encourage you to explore the articles mentioned above to gain a deeper understanding of how recomposition works in Compose:

Hello there 👋

My name is Phat, I work as Mobile Lead Engineer at Lazada, Alibaba. I write about Mobile and Software Engineering in general. Feel free to drop me a message or leave a comment if you spot something interesting in my post. You can always find me on TwitterandGithub.

Happy reading and remember to give it a clap if you have liked the post so far.

 

This article was previously published on proandrdoiddev.com

YOU MAY BE INTERESTED IN

YOU MAY BE INTERESTED IN

blog
It’s one of the common UX across apps to provide swipe to dismiss so…
READ MORE
blog
In this part of our series on introducing Jetpack Compose into an existing project,…
READ MORE
blog
In the world of Jetpack Compose, where designing reusable and customizable UI components is…
READ MORE
blog

How to animate BottomSheet content using Jetpack Compose

Early this year I started a new pet project for listening to random radio…
READ MORE
Menu