Performance with Jetpack Compose — Part 2 | by Udit Verma | Jun, 2023

Whenever a state changes, compose runtime triggers the recomposition of the nearest restartable function in the parent UI tree. Ensuring that a state is read later in the UI tree, ensures that a smaller section of the UI is recomposed whenever that state changes.

@Composable
fun ToDoItem(task: String)
val isDone = remember mutableStateOf(false)
Row
Text(task)
CheckBox(isChecked = isDone, onCheckChange = isDone = !isDone

In the example above, any change in the isDone state would trigger a recomposition of ToDoItem since it’s the nearest skippable function. Row being an inline non-skippable function will be recomposed as well. Text will be skipped since its state (task) hasn’t changed and then the checkbox will be recomposed with the new state.

If the state is hoisted up in the UI tree, we can use lambda to wrap our state reads and pass this lambda to our child composable instead to defer state reads.

Let’s try to understand this better using our example. In our parent composable ComposePerformanceScreen, we have the scroll state for our scrollable column. This state is required by child composables to

  1. decide the position of ScrollPositionIndicator
  2. toggle visibility of ScrollToTopButton

Let’s take a look at the logs to verify what composables are recomposed whenever the scroll state changes.

The parent composable does not rely on this state for its own UI. This state is only read by the parent composable to pass it down to its children. If we are able to defer read of this state to child composables, whenever this state changes, our parent composable (ComposePerformanceScreen) wouldn’t have to recompose. Let’s try to wrap the state reads in lambda functions and pass that lambda instead to child composables.

Before

@Composable
private fun ScrollPositionIndicator(
modifier: Modifier = Modifier,
progress: Float
)
@Composable
private fun ScrollToTopButton(
isVisible: Boolean,
modifier: Modifier = Modifier,
onClick: () -> Unit
)

Changing these states to lambda

@Composable
private fun ScrollPositionIndicator(
modifier: Modifier = Modifier,
progress: () -> Float
)
@Composable
private fun ScrollToTopButton(
isVisible: () -> Boolean,
modifier: Modifier = Modifier,
onClick: () -> Unit
)

Now let’s try to pass in these lambdas from our parent composable

ScrollPositionIndicator(progress =  scrollState.value / (scrollState.maxValue * 1f) )
ScrollToTopButton(
isVisible =
Logger.d(
message = "Recalculating showScrollToTopButton",
filter = LogFilter.ReAllocation
)
scrollState.value / (scrollState.maxValue * 1f) > .5
,
onClick = {
scope.launch
scrollState.scrollTo(0)

)

Awesome, looks like we were able to wrap state reads inside lambda functions and now they are effectively read-only inside the child composables. Hence whenever the scroll state updates, the nearest recomposition scope is now the child composables themselves. Let’s try to look at the logs again

Nice! We no longer see the statement “Recomposing entire Screen” meaning the parent composable is no longer being recomposed on scroll state change.

The source code for this step can be accessed here.

Next Post

23% have fully adopted, 50% still learning, 16% yet to begin

In just 48 hours we received around 400 responses to our poll question about Google Analytics 4. With the standard version of Universal Analytics sunsetting tomorrow (July 1), we asked you: What level of readiness are you (and/or your team) at when it comes to switching to GA4 from Universal […]
23% have fully adopted, 50% still learning, 16% yet to begin

You May Like