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:

  1. Composition: determines what the user interface should look like based on composable functions in your UI tree.

  2. 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.

  3. 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.

Further reading

Previous
Previous

Composable functions and return types

Next
Next

Using MotionLayout in Compose