Search code examples
androidandroid-jetpack-compose

How to avoid recomposition of neibour Box?


I'm trying to draw a backgroud (5000 dots using drawPoints) that shouldn't change once it was drawn. Every time I modify the count variable (which is not read by Box1), Box1 still redraws, though only Box2 reads count variable.

Related code:

class MainActivity : ComponentActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        setContent {
            TestProjectTheme {
                Scaffold(
                    containerColor = Color(0xFF5E5E5E),
                    modifier = Modifier.fillMaxSize()
                ) { innerPadding ->
                    val count = remember { mutableIntStateOf(0) }
                    val incrementCounter = { count.intValue++ }

                    Box(
                        Modifier
                            .size(300.dp)
                            .background(Color.Red)
                            .padding(innerPadding)
                    ) {
                        // Box1
                        Box(Modifier
                            .size(300.dp)
                            .drawWithCache {
                                onDrawBehind {
                                    Log.e("-->", "DrawBehind") // <-- Why is this called even if only Box2 content changed?
                                }
                            }
                            .pointerInput(1, 2) {
                                detectTapGestures { offset ->
                                    incrementCounter()
                                }
                            }) {}

                        // Box2
                        Box(
                            Modifier
                                .size(300.dp)
                        ) {
                            repeat(count.intValue) {
                                Text("$it")
                            }
                        }
                    }
                }
            }
        }
    }
}

How to prevent recomposition of Box1 after modifing counter?

Is there a better way of drawing a static background consisting of 5000 dots?


Solution

  • It happens because of 2 reasons. First one is scopping. Any non-inline @Composable function that returns Unit is a scope in Jetpack Compose. You can refer this answer for more details.

    In you case closest scope is Scaffold, By adding a Composable with scope you can limit recomposition for a smaller scope

        @Composable
        fun MyBox(
            content: @Composable () -> Unit,
        ) {
            Box {
                content()
            }
        }
    
    
    @Preview
    @Composable
    fun Test() {
        val count = remember { mutableIntStateOf(0) }
        val incrementCounter = { count.intValue++ }
    
        SideEffect {
            println("Recomposed with ${count.value}")
        }
        Box(
            Modifier
                .size(300.dp)
                .background(Color.Red)
        ) {
            // Box1
            Box(Modifier
        //        .graphicsLayer()
                .size(300.dp)
                .drawWithCache {
                    onDrawBehind {
                        println("DrawBehind")
                    }
                }
                .pointerInput(1, 2) {
                    detectTapGestures { offset ->
                        incrementCounter()
                    }
                }) {}
    
    
    //        MyBox {
                // Box2
                Box(
                    Modifier
                        .size(300.dp)
                ) {
                    repeat(count.intValue) {
                        Text("$it")
                    }
                }
    //        }
        }
    }
    

    Uncomment MyBox in the example above and you won't see any logs after recomposition in SideEffect.

    Second issue is or how Jetpack Compose works is everything is drawn to same layer unless you use Modifier.graphicsLayer or any Modifier such as clipToBounds, alpha, rotate, etc that calls Modifier.graphics under the hood.

    If you add new layer for, uncomment graphicsLayer(), you will see that that log inside draw Modifier won't be called either. You can refer this link for my question in detail that shows irrelevant composable calling draw based on same issue or how compose works.

    Jetpack Compose deferring reads in phases for performance