3

Consider snippet below

fun doSomething(){
}

@Composable
fun A() {
    Column() {
        val counter = remember { mutableStateOf(0) }
        B {
            doSomething()
        }
        Button(onClick = { counter.value += 1 }) {
            Text("Click me 2")
        }
        Text(text = "count: ${counter.value}")
    }
}

@Composable
fun B(onClick: () -> Unit) {
    Button(onClick = onClick) {
        Text("click me")
    }
}

Now when pressing "click me 2" button the B compose function will get recomposed although nothing inside it is got changed.

Clarification: doSomething is for demonstration purposes. If you insist on having a practical example you can consider below usage of B:

B{
    coroutinScope.launch{
        bottomSheetState.collapse()
        doSomething()
    }
}

My questions:

  1. Why this lamda function causes recomposition
  2. Best ways to fix it

My understanding of this problem

From compose compiler report I can see B is an skippable function and the input onClick is stable. At first I though its because lambda function is recreated on every recomposition of A and it is different to previous one. And this difference cause recomposition of B. But it's not true because if I use something else inside the lambda function, like changing a state, it won't cause recomposition.

My solutions

  1. use delegates if possible. Like viewmode::doSomething or ::doSomething. Unfortunately its not always possible.
  2. Use lambda function inside remember:
val action = remember{
    {
        doSomething()
    }
}
B(action)

It seems ugly =) 3. Combinations of above.

3 Answers3

4

When you click the Button "Click me 2" the A composable is recomposed because of Text(text = "count: ${counter.value}"). It happens because it recompose the scope that are reading the values that can change.

If you are using something like:

B {
    Log.i("TAG","xxxx")
}

the B composable is NOT recomposed clicking the Button "Click me 2".

If you are using

B{
    coroutinScope.launch{
        Log.i("TAG","xxxx")
    }
}

the B composable is recomposed.

When a State is read it triggers recomposition in nearest scope. And a scope is a function that is not marked with inline and returns Unit.
To use a coroutinScope you have to use rememberCoroutineScope that is a composable inline function. The the body of inline composable functions are simply copied into their call sites, such functions do not get their own recompose scopes.

To avoid it you can use:

B {
    Log.i("TAG","xxxx")
}

and

@Composable
fun B(onClick: () -> Unit) {
    val scope = rememberCoroutineScope()
   
    Button(
        onClick = {
            scope.launch {
                onClick()
            }
        }
    ) {
        Text(
            "click me ",
        )
    }
}

Sources and credits:

You can use the LogCompositions composable described in the 2nd post to check the recomposition in your code.

Gabriele Mariotti
  • 320,139
  • 94
  • 887
  • 841
  • Thank you, Gabriele. Unfortunately, this is not answering my problem. Consider a case in that you want to launch something that escapes the scope of the B function. For example, you launch something that hides B with animation and then do something after the animation end. In this case, if you create the scope inside B it will fail because it will get canceled as soon as B gets out of the composition. You also can't pass the scope to B as it is unstable. One way is to use scope inside a lambda in A. This also causes recomposition and why it happen was my question. Hopefully, I found the answer – Ali Golmirzaei Feb 07 '23 at 08:58
  • 1
    Maybe hiding with animation was not a good example. But, consider any suspending function that in the end causes B to get out of the composition. – Ali Golmirzaei Feb 07 '23 at 09:06
  • sorry that I didn't provide enough information in my question. – Ali Golmirzaei Feb 07 '23 at 11:10
1

Generally speaking, if you are using a property inside a lambda function that is unstable, it causes the child compose function unskippable and thus gets recomposed every time its parent gets recomposed. This is not something easily visible and you need to be careful with it. For example, the bellow code will cause B to get recomposed because coroutinScope is an unstable property and we are using it as an indirect input to our lambda function.

fun A(){
    ...
    val coroutinScope = rememberCoroutineScope()
    B{
        coroutineScope.launch {
            doSomething()
        }
    }
}

To bypass this you need to use remember around your lambda or delegation (:: operator). There is a note inside this video about it. around 40:05

There are many other parameters that are unstable like context. To figure them out you need to use compose compiler report.

Here is a good explanation about the why: https://multithreaded.stitchfix.com/blog/2022/08/05/jetpack-compose-recomposition/

0

Sometimes function reference viewModel::doShomething also triggers recomposition. In that case and solution that works is to capture lambda inside remember

val onClick = remember {
    { index: Int ->
        viewModel.toggleSelection(index)
    }
}

You can see the answer here

Why does a composable recompose while seemingly being stateless (the only passed parameter is a function, not a state)

or stability edit section when you use on clicks inside LazyColumn or other Lazy lists.

Jetpack Compose lazy column all items recomposes when a single item update

Thracian
  • 43,021
  • 16
  • 133
  • 222