One Off the Slack: One State, or Two?
Landry Norris asked:
In general, will individual state variables be faster than a single data class that holds all the state for a screen? I know that when using individual variables, Compose will only re-render Composables that use the value of the variable that was changed. When using a single data class, is Compose smart enough to only re-render Composables that use the fields that changed, or will it re-render every Composable that uses any field of the single state variable?
I was reading through https://developer.android.com/jetpack/compose/phases#3-phases and saw this quote: “When the state value changes, the recomposer schedules reruns of all the composable functions which read that state value”, which makes me think it re-renders every Composable that uses the single variable, without skipping those that read fields that are the same.
Google’s Adam Powell responded:
Skipping has to do with determining stable equality. To skip, all parameters to a
@Composable fun
must be known to be stable, and their new values must compare.equals
to their old values.
Your data class might be stable, but if it’s part a very deep tree of data classes,
.equals
might take a while
Adam also asked for a code sample, so Landry complied:
Let’s say I have two text fields that get their text from variables,
Column {
Text(text1)
Text(text2)
}
in the case of multiple variables, or
Column {
Text(state.text1)
Text(state.text2)
}
in the case of a data class. You could have text1 and text2 as two different state variables, where I know that running text1.value = “foo” only re-renders the first text, or you could have a data class ScreenState(text1, text2). In the second case, if I were to run
state.value = state.value.copy(text1 = "foo")
, would this be identical to the first case, causing only the top Text to get re-rendered, or will both get re-rendered due to changing the value of the single state variable?
Adam then clarified:
These will cause identical recomposition behavior, here’s why:
Kotlin function calls are pass by value, so any read happens before the function call. That’s what is tracked for invalidation.
var text1 by remember { mutableStateOf(...) }
var text2 by remember { mutableStateOf(...) }
// ...
Column {
Text(text1) // call to getValue happens while evaluating parameters to pass
Text(text2) // call to getValue happens while evaluating parameters to pass
}
So the containing recompose scope of the
Text
call is what is invalidated in both cases
and since
Column
is inline, the containing recompose scope is the containing scope ofColumn
both of the snippets you posted rely on
Text
skipping when the string passed to it is the same as the previous recomposition
the scope of invalidation is determined by where the
.value
(or delegategetValue
) occurs
Landry continued:
I see. So both Texts get recomposed, but the second one detects that the value is the same in both cases, so it doesn’t re-render, while the first one does in both cases, since the value has changed. Is my understanding correct?
Adam replied:
the caller of both
Texts
gets recomposed. Both calls toText
happen. The function body skipping behavior added to theText
function by the compose compiler plugin compares the parameters of skippable composable functions and determines whether to return early without doing work or not
Read the original thread in the kotlinlang Slack workspace. Not a member? Join that Slack workspace here!