Search code examples
androidandroid-jetpack-composecompose-recomposition

Lambda function used as input argument causes recomposition


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.


Solution

  • 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/