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 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 {

in the case of multiple variables, or

Column {

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 of Column

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 delegate getValue) 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 to Text happen. The function body skipping behavior added to the Text 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!