1

Consider we have the following structure in a toggling implementation scenario:

@Composable
fun RootComposable() {
  var someAuxToggle by remember { mutableStateOf(false) }

  if (someAuxToggle) {
    FirstComposable { someAuxToggle = !someAuxToggle }
  } else {
    SecondComposable { someAuxToggle = !someAuxToggle }
  }
}

@Composable
fun FirstComposable(auxCallback: () -> Unit) {
  val myRandomNumber by remember { mutableStateOf(Random.nextInt(100)) }
  Button(onClick = { auxCallback() }) {
    Text(text = "I'm the FirstComposable.\nMy random is: $myRandomNumber")
  }
}

@Composable
fun SecondComposable(auxCallback: () -> Unit) {
  val myRandomNumber by remember { mutableStateOf(Random.nextInt(100)) }
  Button(onClick = { auxCallback() }) {
    Text(text = "I'm the SecondComposable.\nMy random is: $myRandomNumber")
  }
}

Doing this, the toggling works, the composables are correclty called, however their own state changes, once I'm "recreating" then. This is also valid if those buttons was placed inside other top composables and these top composables be called.

Now, if we implement this in a little hacky way the state keeps alive (the random numbers are not regenerated):

@Composable
fun RootComposable() {
  var someAuxToggle by remember { mutableStateOf(false) }

  Box(
    if (someAuxToggle)
      Modifier.wrapContentSize()
    else
      Modifier.size(0.dp)
  ) {
    FirstComposable { someAuxToggle = !someAuxToggle }
  }

  Box(
    if (someAuxToggle)
      Modifier.size(0.dp)
    else
      Modifier.wrapContentSize()
  ) {
    SecondComposable { someAuxToggle = !someAuxToggle }
  }
}

@Composable
fun FirstComposable(auxCallback: () -> Unit) {
  val myRandomNumber by remember { mutableStateOf(Random.nextInt(100)) }
  Button(onClick = { auxCallback() }) {
    Text(text = "I'm the FirstComposable.\nMy random is: $myRandomNumber")
  }
}

@Composable
fun SecondComposable(auxCallback: () -> Unit) {
  val myRandomNumber by remember { mutableStateOf(Random.nextInt(100)) }
  Button(onClick = { auxCallback() }) {
    Text(text = "I'm the SecondComposable.\nMy random is: $myRandomNumber")
  }
}

Note that in this implementation both buttons are being called inside root but the toggling was made by switching their "visibility", via setting their sizes.

Also note that the buttons composable are really the same component, but my idea is just demonstrate the state behaviour.

My question is about the best way to keep the state if we call a composable again, like in the first code; if there are a simple way to do it or if we really need to store those states in some top level fields or something like this.

Also, is the second approach too expensive in terms of performance? I think yes, because both composable are "turned on" all the time, then if we have composables that makes hard work (e.g., via side effects or even background tasks) then performance can be lowered.

Anyway, I hope we find a solution to that.

Lucas Sousa
  • 192
  • 3
  • 14
  • [State hoisting](https://developer.android.com/jetpack/compose/state#state-hoisting) is a pretty integral part of structuring your app in Compose. – ianhanniballake Jun 15 '22 at 02:28
  • Sure, but I'm wondering how to deal with the problem if the composable caller is in much levels up instead to the direct upper composable.Correct if I'm wrong, but hoisting seems more appropriate to pass state up 1 single level. If we need pass multiple levels the code appears repeat a lot. – Lucas Sousa Jun 15 '22 at 18:23

1 Answers1

2

It depends on the scope of how you want to recreate or restore a state, you can either hoist it in a viewmodel where a state is dependent on its lifecycle, or inside a rememberSaveable where you have to define a Saver for it to save and restore the state. So far, I only consider using rememberSaveable when I only want to survive and restore states during configuration changes (i.e light to dark, screen rotate), on the other hand, I hoist states via ViewModel when functionalities of a composable depends on much deeper functionalities (i.e repository/network calls, heavy business use-cases)

Also Both of the implementation will trigger the entire re-composition of RootComposable as it observes a mutableState

 if (someAuxToggle) 
 ...
 ...

I'm not quite sure though if the second one has a significant performance hit assuming this is only the scope of your actual code.

I'd recommend watching this once in a while as a refresher when making hoisting decisions A Compose state of mind: Using Jetpack Compose's automatic state observation

z.g.y
  • 5,512
  • 4
  • 10
  • 36