Theme message: Psychologists have found that green can make the mood relaxed

Many students will be beginner Compose Recomposition of Composable (official documents translated into “restructuring”) heart of worry, worry about a wide range of restructuring would affect performance.

The Compose compiler does a lot of work in the background to keep the recomposition range as small as possible, thus avoiding invalid overhead:

Recomposition skips as much as possible When portions of your UI are invalid, Compose does its best to recompose just the portions that need to be updated. The developer.android.com/jetpack/com…

So what is the scope of code execution when recombination occurs? Let’s test this with an example:

@Composable
fun Foo(a) {
    var text by remember { mutableStateOf("") }
    Log.d(TAG, "Foo")
    Button(onClick = {
        text = "$text $text"
    }.also { Log.d(TAG, "Button") }) {
        Log.d(TAG, "Button content lambda")
        Text(text).also { Log.d(TAG, "Text")}}}Copy the code

As shown above, when a button is clicked, the change in State triggers recomposition.

Think about what the log output looks like at this point

.

You can find the answer at the bottom of the article. Does it agree with your judgment?


How does Compose determine the scope of reorganization?

Compose analyzes the code blocks affected by a state change at compile time, records their references, and when the state changes, finds the code blocks based on the references and marks them as Invalid. Compose triggers recomposition before the next render frame arrives and executes the Invalid block during the recomposition process.

The Invalid code block is the scope the compiler finds for the next reorganization. Code that can be marked as Invalid must be non-inline @Composalbe function/lambda with no return value and must follow the reorganization scope minimization principle.

Why is it non-inline with no return value (return Unit)?

For inline functions, because they are expanded at compile time at the call point, the appropriate call entry cannot be found at the next reorganization and the caller’s reorganization scope can only be shared.

A function with a return value cannot be reorganized alone because changes in the return value affect the caller. Instead, it must be reorganized with the caller, so it cannot be marked as invalid as an entry point

Range minimization principle

Only code blocks that are affected by state changes participate in the reorganization, and code that does not depend on state does not participate in the reorganization.

Now that you know the basic rules for Compose’s redraw scope, let’s go back to the example at the beginning of this article and try to answer the following question:


Why not just Text?

When clicking button, MutableState changes, and the only place in the code to access this state is Text(…). , why not just reorganize Text(…) , but Button {… } the entire curly brace?

The first thing to understand is the occurrence of Text(…) The text in the argument is actually an expression

The following two notations are equivalent in order of execution

Println (" hello "+" world ")Copy the code
valArg = "hello" + "world" println(arg)Copy the code

Always “hello” + “world” is executed first as an expression, followed by the println method call.

Returning to the previous example, the argument text is called as an expression at the end of the lambda of the Button, and text () is later passed as an argument. So the minimum recombination range is the lambda after the Button and not the Text()


Does Foo participate in the reorganization?

Following the scope minimization principle, Foo does not have any access to state, so it is easy to know that Foo should not participate in the reorganization.

One thing to note is that in the example Foo declares text by proxy. What if = was assigned to text directly?

@Composable fun Foo(a) {
  val text: MutableState<String> = remember { mutableStateOf("") }

  Button(onClick = { 
  	 text = "$text $text"
  }) {
    Text(text.value)
  }
}
Copy the code

The answer is the same, still not participating in restructuring.

First, Compose cares about whether there is a read to state in the code block, not a write.

Second, the = does not mean that text will be assigned a new object, because the MutableState instance to which text refers will never change, only the internal value


Why didn’t Button participate in the restructuring?

This is easy to explain. Foo, the Button’s caller, does not participate in the reorganization, and Button does not participate in the reorganization. Only the trailing lambda does.


Is Button’s onClick part of the reorganization?

The restructure scope must be function/lambda of @composable, onClick is a normal lambda and therefore independent of the restructure logic.


Attention! Inline trap in reorganization!

As mentioned earlier, it’s important to understand that only non-inline functions qualify as the minimum range for recombination!

We changed the code slightly by wrapping a Box{… }

@Composable
fun Foo(a) {

    var text by remember { mutableStateOf("") }

    Button(onClick = { text = "$text $text" }) {
        Log.d(TAG, "Button content lambda")
        Box {
            Log.d(TAG, "Box")
            Text(text).also { Log.d(TAG, "Text")}}}}Copy the code

The log is as follows:

D/Compose: Button content lambda
D/Compose: Boxt
D/Compose: Text
Copy the code

Why doesn’t the refactoring scope start with Box?

The Composable container class Column, Row, Box, and even Layout are all inline functions, so they only share the caller’s reorganization scope, which is the Button’s tail lambda

What if you want to improve performance by narrowing the recombination scope?

@Composable
fun Foo(a) {

    var text by remember { mutableStateOf("") }

	Button(onClick = { text = "$text $text" }) {
        Log.d(TAG, "Button content lambda")
        Wrapper {
            Text(text).also { Log.d(TAG, "Text")}}}}@Composable
fun Wrapper(content: @Composable() - >Unit) {
    Log.d(TAG, "Wrapper recomposing")
    Box {
        Log.d(TAG, "Box")
        content()
    }
}
Copy the code

As shown above, customize the non-inline function to satisfy the Compose recombination range minimization condition.


conclusion

Just don’t rely on side effects from recomposition and compose will do the right thing — Compose Team

The specific rules for the scope of reorganization are not detailed in the official document. That’s because developers need to keep in mind that Compose’s compile-time optimization ensures that recomposition always works the way it should, and that it should be developed in the most natural way possible, without the additional learning costs associated with these specific rules.

However, there is one thing to keep in mind as a developer:

Do not write logic containing side effects directly into a Composable!

Side effects cannot be repeated with recomposition, so we need to keep the Composable “pure”.

You can’t just assume that a function/lambda doesn’t participate in recombination, and accidentally embed some side effect code that makes it impure. Since we can’t be sure that there is an inline trap, even if we could, there is no guarantee that the current optimization rules won’t change in the future.

So the safest thing to do is to write the side effects to LaunchedEffect{}, DisposableEffect{}, SideEffect{}, and use remeber{}, derivedStateOf{} to handle those time-consuming calculations.



The result of the initial example:

D/Compose: Button content lambda
D/Compose: Text
Copy the code