Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

When does Compose finalize a frame?

Assuming 60 fps over 1000 ms gives 16,6 ms per frame. Now assume Compose can render my code in 2 ms, let's call this frame_A. For 5 ms everything is idle. Then a LaunchedEffect changes some state, a recompose occurs. Recompose takes again 2 ms, let's call this frame_B.

So frame_A and frame_B are both created within the 16,6 ms time window that one has for one frame. Will only frame_A be drawn or only frame_B or both?

like image 238
stefan.at.wpf Avatar asked Jul 01 '26 11:07

stefan.at.wpf


2 Answers

In this scenario both frame_A and frame_B could be drawn within the 16.6ms for a single frame.

Possibilities:

  1. Android System: Android manages a queue for drawing UI elements so when your compose UI requested for recomposition, the system simply adds to queue.
  2. Compose Framework: Recomposition in compose framework may not block the previous frames rendering, the latest UI can be updated in next frame.

Overall, both will be drawn.

Worst case scenario [depends on available resources for system to use]

Priority: Android might give priority to drawing the most recent UI state (frame_B in your case). frame_A might be skipped in that case.

Read more about recomposition.

like image 113
Aks4125 Avatar answered Jul 04 '26 01:07

Aks4125


Jetpack Compose has three phases of a frame.

Compose has three main phases:

Composition: What UI to show. Compose runs composable functions and creates a description of your UI.

Jetpack Compose deferring reads in phases for performance

Layout: Where to place UI. This phase consists of two steps: measurement and placement. Layout elements measure and place themselves and any child elements in 2D coordinates, for each node in the layout tree.

Drawing: How it renders. UI elements draw into a Canvas, usually a device screen.

When recomposition happens it depends on scope and skippability to occurs. When recomposition happens layout or and draw phases can be skipped as well.

Layout phase can be skipped if Composable doesn't need to update dimensions. You can refer this answer when and how layout phase is skipped.

Draw phase

In Jetpack Compose unless you use Modifier.graphicsLayer everything is drawn into same layer for performance which means when a Composable is drawn sibling is also drawn even if it doesn't have any recomposition or layout doesn't need to change.

Let's create a Composable that draws and logs recomposition inside SideEffect

@Composable
fun MyComposable(
    modifier: Modifier = Modifier,
    counter: Int
) {

    val textMeasurer = rememberTextMeasurer()
    SideEffect {
        println("MyComposable counter: $counter")
    }

    Canvas(modifier) {
        drawText(textMeasurer, "Counter: $counter")
    }
}

And demo to show how applying Modifier.graphicsLayer() or Modifier.graphicsLayer{} effects when a Composable is drawn.

A [Modifier.Element] that makes content draw into a draw layer. The draw layer can be invalidated separately from parents. A [graphicsLayer] should be used when the content updates independently from anything above it to minimize the invalidated content.

@Preview
@Composable
private fun DrawTest() {
    var counter1 by remember {
        mutableIntStateOf(0)
    }

    var counter2 by remember {
        mutableIntStateOf(0)
    }

    Column(
        modifier = Modifier.fillMaxSize().drawBehind {
            println("Column is drawing...")
        }
    ) {

        Button(
            modifier = Modifier
                .drawWithContent {
                    drawContent()
                    println("Drawing Button1")
                },
            onClick = {
                counter1++
            }
        ) {
            Text("Counter1: $counter1")
        }

        Button(
            modifier = Modifier
                .graphicsLayer()
                .drawWithContent {
                    drawContent()
                    println("Drawing Button2")
                },
            onClick = {
                counter2++
            }
        ) {
            Text("Counter2: $counter2")
        }

        MyComposable(
            modifier = Modifier
                .size(100.dp)
                .layout { measurable, constraints ->
                    val placeable = measurable.measure(constraints)
                    println("MyComposable1 MeasureScope")

                    layout(placeable.width, placeable.height) {
                        placeable.placeRelative(0, 0)
                    }
                }
                .drawWithContent {
                    println("MyComposable1 drawing...")
                    drawContent()
                },
            counter = counter1
        )


        Text("Sample Text", modifier = Modifier.drawWithContent {
            println("Text drawing...")
            drawContent()
        })

        MyComposable(
            modifier = Modifier
                .graphicsLayer()
                .size(100.dp)
                .layout { measurable, constraints ->
                    val placeable = measurable.measure(constraints)
                    println("MyComposable2 MeasureScope")

                    layout(placeable.width, placeable.height) {
                        placeable.placeRelative(0, 0)
                    }
                }
                .drawWithContent {
                    println("MyComposable2 drawing...")
                    drawContent()
                },
            counter = counter2
        )
    }
}

When you update counter1 by clicking button it logs

 I  MyComposable counter: 3
 I  Column is drawing...
 I  Drawing Button1
 I  MyComposable1 drawing...
 I  Text drawing...

Which means when MyComposable calls draw also Column, top Button, Text and MyComposable even though they are not recomposed. This is as in your question if you draw frame_A so is frame_B drawn.

When you update counter2 it logs

 I  MyComposable counter: 2
 I  MyComposable2 drawing...

As you can see we are only recomposing MyComposable2, without calling layout phase and only drawing it inside its layer instead of drawing sibling or parent Composables.

Using a layer makes your Composable prevents Composable from being drawn unnecessarily as in this question you can see sibling Canvas is drawn while no draw is required.

Unable to stop the Canvas Composable's draw scope from triggering continuously

Also Modifier.alpha(), Modifier.clip(), Modifier.rotate and Modifiers that are available as params of Modifier.graphicsLayer also use layer under the hood.

@Stable
fun Modifier.alpha(
    /*@FloatRange(from = 0.0, to = 1.0)*/
    alpha: Float
) = if (alpha != 1.0f) graphicsLayer(alpha = alpha, clip = true) else this


@Stable
fun Modifier.clip(shape: Shape) = graphicsLayer(shape = shape, clip = true)


@Stable
fun Modifier.shadow(
    elevation: Dp,
    shape: Shape = RectangleShape,
    clip: Boolean = elevation > 0.dp,
    ambientColor: Color = DefaultShadowColor,
    spotColor: Color = DefaultShadowColor,
) = if (elevation > 0.dp || clip) {
    inspectable(
        inspectorInfo = debugInspectorInfo {
            name = "shadow"
            properties["elevation"] = elevation
            properties["shape"] = shape
            properties["clip"] = clip
            properties["ambientColor"] = ambientColor
            properties["spotColor"] = spotColor
        }
    ) {
        graphicsLayer {
            this.shadowElevation = elevation.toPx()
            this.shape = shape
            this.clip = clip
            this.ambientShadowColor = ambientColor
            this.spotShadowColor = spotColor
        }
    }
} else {
    this
}


@Stable
fun Modifier.graphicsLayer(
    scaleX: Float = 1f,
    scaleY: Float = 1f,
    alpha: Float = 1f,
    translationX: Float = 0f,
    translationY: Float = 0f,
    shadowElevation: Float = 0f,
    rotationX: Float = 0f,
    rotationY: Float = 0f,
    rotationZ: Float = 0f,
    cameraDistance: Float = DefaultCameraDistance,
    transformOrigin: TransformOrigin = TransformOrigin.Center,
    shape: Shape = RectangleShape,
    clip: Boolean = false,
    renderEffect: RenderEffect? = null,
    ambientShadowColor: Color = DefaultShadowColor,
    spotShadowColor: Color = DefaultShadowColor,
    compositingStrategy: CompositingStrategy = CompositingStrategy.Auto
)
like image 30
Thracian Avatar answered Jul 04 '26 02:07

Thracian