Compose phases and optimizations
May 13, 2022
There is an excellent talk from Google I/O 2022 titled Performance best practices for Jetpack Compose. A part of it includes an example of a potential optimization related to Compose phases. This blog post is meant to spread that knowledge using a different medium.
Jetpack Compose phases
Compose renders each frame through 3 distinct phases:
Composition: determines what the user interface should look like based on composable functions in your UI tree.
Layout: Measures user interface widgets and decides where to place them on the screen. Modifiers and composables from the Composition phase are taken into account to complete this phase.
Drawing: Draws the user interface elements to the screen based on measurements provided by the Layout phase.
For more details, on Compose Phases, see https://developer.android.com/jetpack/compose/phases
These 3 phases are repeated for every frame and each phase uses computing power. There are instances where performance can be improved by skipping one or more Compose phases. Let’s look at a practical easy to digest example below.
Inline with the Google I/O example, let’s create a simple 200x200 square and repeatedly animate its color between initialValue
to targetValue
and back.
@Composable fun MyBox() { val color by animateColorBetween(Color.Red, Color.Green) Box( modifier = Modifier .wrapContentSize(Alignment.Center) .width(200.dp) .height(200.dp) .background(color) ) { println("Recomposing") } } @Composable private fun animateColorBetween( initialValue: Color, targetValue: Color ): State<Color> { val infiniteTransition = rememberInfiniteTransition() return infiniteTransition.animateColor( initialValue = initialValue, targetValue = targetValue, animationSpec = infiniteRepeatable( animation = tween(2000), repeatMode = RepeatMode.Reverse ) ) }
Which will result in the following UI:
The problem with the code above is that every time a color changes (on every frame!), our Box
will recompose. Notice the println(“Recomposing”)
in the content lambda. When the UI renders, the Logcat will update repeatedly:
20:30:35.282 18878-18878/com.example.demo I/System.out: Recomposing 20:30:35.300 18878-18878/com.example.demo I/System.out: Recomposing 20:30:35.321 18878-18878/com.example.demo I/System.out: Recomposing 20:30:35.336 18878-18878/com.example.demo I/System.out: Recomposing
Skipping phases for better performance
Since the only thing that changes on every frame is our Box color, we should be able to redraw the Box using Draw phase and not repeat the Composition and Layout phases every time the color changes.
To do that, we can explicitly ask for Draw phase every time color changes:
To do that, we change our Modifier
to draw on Canvas with help of lambda-based modifier drawBehind
:
@Composable fun MyBox() { val color by animateColorBetween(Color.Red, Color.Green) Box( modifier = Modifier .wrapContentSize(Alignment.Center) .width(200.dp) .height(200.dp) .drawBehind { drawRect(color) } ) { println("Recomposing") } }
Instead of using .background
we use .drawBehind
that accesses DrawScope
directly whenever color changes. Therefore, only the Draw
phase gets re-executed when color changes.
For more detailed post on drawing on Canvas, see my post Getting started with Canvas in Compose.
Our UI won’t change but rendering is now more efficient—the Composition and therefore Layout phases only happened once as evident by the Logcat. Only the Draw
phase needed to be repeated.
20:43:04.570 19244-19244/demo I/System.out: Recomposing
fun drawRect
is a function instance (not a composable function) that reads the color state. The function instance itself does not change and therefore does not require re-composition or a new layout path.