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?
In this scenario both frame_A and frame_B could be drawn within the 16.6ms for a single frame.
Possibilities:
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.
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.
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
)
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With