Jetpack Compose deferring reads in phases for performance
Asked Answered
S

1

9

Official article about Jetpack Compose phases and this article about deferring reads for performance suggests moving any state reads from Composition phase to Layout or Draw phases such as

Modifier.offset() which is read in Composition phase to Modifier.offset{} which is read in Layout phase or

Modifier.background() to Modifier.drawBehind{} to defer read to Draw phase.

Based on the articles i created 2 separate samples.

For this created a Box that changes title color on each recomposition

@Composable
private fun MyBox(modifier: Modifier, title: String) {

    LogCompositions(msg = "πŸ”₯ MyBox() COMPOSITION $title")

    Column(modifier) {

        // This Text changes color on every recomposition
        Text(
            text = title,
            modifier = Modifier
                .background(getRandomColor())
                .fillMaxWidth()
                .padding(2.dp)
        )

        Text(
            text = "modifier hash: ${modifier.hashCode()}\n"
                    + "Modifier: $modifier",
            color = Color.White,
            modifier = Modifier.heightIn(max = 120.dp),
            fontSize = 10.sp
        )
    }
}

function for generating color

fun getRandomColor() =  Color(
    red = Random.nextInt(256),
    green = Random.nextInt(256),
    blue = Random.nextInt(256),
    alpha = 255
)

Log recompositions

class Ref(var value: Int)

// Note the inline function below which ensures that this function is essentially
// copied at the call site to ensure that its logging only recompositions from the
// original call site.
@Composable
inline fun LogCompositions(msg: String) {
    val ref = remember { Ref(0) }
    SideEffect { ref.value++ }
    println("$msg, count: ${ref.value}")
}

First Sample

@Composable
private fun PhasesSample1() {

    LogCompositions(msg = "1️⃣ PhasesSample1")

    var offsetX by remember { mutableStateOf(0f) }

    Row(verticalAlignment = Alignment.CenterVertically) {
        Text(text = "OffsetX")
        Spacer(modifier = Modifier.width(5.dp))
        Slider(value = offsetX,
            valueRange = 0f..50f,
            onValueChange = {
                offsetX = it
            }
        )
    }

    val modifier1 = Modifier
        // Reads value directly
        .offset(x = offsetX.dp)
        .layout { measurable, constraints ->
            val placeable: Placeable = measurable.measure(constraints)
            layout(placeable.width, placeable.height) {
                println("πŸ˜ƒοΈ modifier1 LAYOUT")
                placeable.placeRelative(0, 0)
            }
        }
        .background(Blue400)
        .drawWithContent {
            println("😜 modifier1 DRAW")
            drawContent()
        }


    val modifier2 = Modifier
        // Deferring state to Layout phase prevents
        // Composables that have this modifier to be recomposed
        .offset {
            val newX = offsetX.dp.roundToPx()
            IntOffset(newX, 0)
        }
        .layout { measurable, constraints ->
            val placeable: Placeable = measurable.measure(constraints)
            layout(placeable.width, placeable.height) {
                println("🍏 modifier2 LAYOUT")
                placeable.placeRelative(0, 0)
            }
        }
        .background(Blue400)
        .drawWithContent {
            println("🍎 modifier2 DRAW")
            drawContent()
        }

    MyBox(modifier = modifier1, "modifier1")
    Spacer(modifier = Modifier.height(8.dp))
    MyBox(modifier = modifier2, "modifier2")
}

This one works as expected. When i change offset with slider modifier1 goes through Composition->Layout->Draw phases since it has Modifier.offset(x = offsetX.dp)

modifier2 calls Layout phase only, i thought it should call Draw too but it's not a major question, probably because it reads same color so it doesn't update Draw phase

Other sample is

@Composable
private fun PhasesSample2() {

    LogCompositions(msg = "2️⃣  PhasesSample2")

    // This state is for triggering recomposition for PhasesSample2,
    // child composables don't read this state
    var someValue by remember { mutableStateOf(0f) }
    Row(verticalAlignment = Alignment.CenterVertically) {
        Text(text = "someValue")
        Spacer(modifier = Modifier.width(5.dp))
        Slider(value = someValue,
            valueRange = 0f..50f,
            onValueChange = {
                someValue = it
            }
        )
    }

    val modifier3 = Modifier
        .layout { measurable, constraints ->
            val placeable: Placeable = measurable.measure(constraints)
            layout(placeable.width, placeable.height) {
                println("πŸš• modifier3 LAYOUT")
                placeable.placeRelative(0, 0)
            }
        }
         .drawWithContent {
            println("πŸš— modifier3 DRAW")
            drawRect(getRandomColor())
            drawContent()
        }

    val modifier4 = Modifier
        .layout { measurable, constraints ->
            val placeable: Placeable = measurable.measure(constraints)
            layout(placeable.width, placeable.height) {
                println("⚾️ modifier4 LAYOUT")
                placeable.placeRelative(0, 0)
            }
        }
        .drawWithContent {
            println("🎾 modifier4 DRAW")
            drawContent()
        }
        // πŸ”₯ reading color causes Composition->Layout->Draw
        .background(getRandomColor())

    MyBox(modifier = modifier3, "modifier3")
    Spacer(modifier = Modifier.height(8.dp))
    MyBox(modifier = modifier4, "modifier4")
}

Both Sample Composables are in a Column

Column(
    modifier = Modifier
        .fillMaxSize()
        .padding(8.dp)
) {
    PhasesSample1()
    Spacer(modifier =Modifier.height(20.dp))
    PhasesSample2()
}

If i move Slider in PhasesSample1 Composable log is as

I/System.out: 1️⃣ PhasesSample1, recomposition: 1
I/System.out: πŸ”₯ MyBox() COMPOSITION modifier1, recomposition: 1
I/System.out: πŸ˜ƒοΈ modifier1 LAYOUT
I/System.out: 🍏 modifier2 LAYOUT
I/System.out: 😜 modifier1 DRAW
I/System.out: πŸš— modifier3 DRAW
I/System.out: 🎾 modifier4 DRAW
I/ViewRootImpl@159ca9[MainActivity]: ViewPostIme pointer 0
I/System.out: 1️⃣ PhasesSample1, recomposition: 2
I/System.out: πŸ”₯ MyBox() COMPOSITION modifier1, recomposition: 2
I/System.out: πŸ˜ƒοΈ modifier1 LAYOUT
I/System.out: 🍏 modifier2 LAYOUT
I/System.out: 😜 modifier1 DRAW
I/System.out: πŸš— modifier3 DRAW
I/System.out: 🎾 modifier4 DRAW

Question

Why DRAW phases of modifier3 and modifier4 are called when they are in a different Composable with scope? As you can see only PhasesSample1 is recomposed.

If i move Slider in PhasesSample2 Composable

I/System.out: 2️⃣  PhasesSample2, recomposition: 1
I/System.out: πŸ”₯ MyBox() COMPOSITION modifier4, recomposition: 1
I/System.out: ⚾️ modifier4 LAYOUT
I/System.out: 😜 modifier1 DRAW
I/System.out: πŸš— modifier3 DRAW
I/System.out: 🎾 modifier4 DRAW
I/System.out: 2️⃣  PhasesSample2, recomposition: 2
I/System.out: πŸ”₯ MyBox() COMPOSITION modifier4, recomposition: 2
I/System.out: ⚾️ modifier4 LAYOUT
I/System.out: 😜 modifier1 DRAW
I/System.out: πŸš— modifier3 DRAW
I/System.out: 🎾 modifier4 DRAW

This time DRAW phase is invoked only modifier1 is called but not modifier2?

This is how visually changes happen. Change of color of title, section with modifierX on top, is due to recomposition also Modifier hash code changes.

Background change when title doesn't change is due to calling DRAW state as you can verify from logs.

enter image description here

Semolina answered 1/6, 2022 at 7:22 Comment(0)
S
1

This happens because by default every Composable draws to same layer. When Composable is updated anything on same layer is drawn. If you wish some section of Composable to be drawn independently you can use Modifier.graphicsLayer{} or other Modifiers that use Modifier.graphicsLayer under the hood such as clipToBounds, alpha, rotate, etc. to draw.

Semolina answered 12/9 at 6:54 Comment(0)

© 2022 - 2024 β€” McMap. All rights reserved.