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
- decide the position of
ScrollPositionIndicator
- 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.