So I'm trying to understand `rememberSaveable` and...
# compose
j
So I'm trying to understand
rememberSaveable
and how something is considered to leave composition. I'm a bit confused because a
rememberSaveable
value sticks around after device rotation, which makes sense. But in the scenario that the
rememeberSaveable
was part of an if/else, and "leaves" the composition on rotation, it's value is still preserved after its block is re-introduced. An example below I get into the
if
with a button click, add the value a few times, rotate the device (causing me to go back to the else) and click the
Go to if
the
rememberSaveable
value is still around, even though it left the composition while the composable was in the
else
?
Copy code
Column {
      var inIf by remember { mutableStateOf(false) }
      if (inIf) {
        var rememberTest by rememberSaveable { mutableIntStateOf(1) }
        Timber.d(rememberTest.toString())
        Button({
          rememberTest += 1
        }) {
          Text("Do it $rememberTest times")
        }
        Button({
          inIf = false
        }) {
          Text("Go to else")
        }
      } else {
        Button({
          inIf = true
        }) {
          Text("Go to if")
        }
      }
    }
I'd expect the
rememberSaveable
to be freshly created if I rotated, it was outside the composition after rotation, and I brought it back into the composition through the conditional
a
This is working as intended, although this case is a bit confusing
rememberSaveable
works by storing the state of whatever is in composition when saved instance state is saved. Upon recreation, state starts getting restored back out of saved instance state - but it isn't required that the state be restored immediately in the first composition. The saved instance state will hang around until something pulls it back out. So in this case, if
inIf
is true when rotating, the saved instance state of
rememberTest
will be saved. After recreation, where
inIf
is false, the saved instance state will remain available but nothing has consumed it yet. When
inIf
becomes
true
, now the saved instance state will be restored from before the recreation. The rule is more like "`rememberSaveable` will lose its state when leaving composition unless the cause of leaving composition is due to the saved instance state being saved"
A contrived example where this behavior is useful:
Copy code
var isLoading by remember { mutableStateOf(true) }
LaunchedEffect(Unit) {
    delay(1000)
    isLoading = true
}

if (isLoading) {
    CircularProgressIndicator()
} else {
    val myState by rememberSaveable { mutableStateOf(0) }
    // ...
}
j
Thanks for following up on this Alex! I ended up getting around the issue by utilizing
movableContentOf
to handle layout changes without having two separate composables with their own state. But this is helpful to know still. Is there a way to know looking inside the
rememberSaveable
code to know how it would differentiate leaving composition normally vs from a configuration change aka saved instance state? When I'm initially looking inside it I'm guessing maybe it maintains the same compositeKey on rotation vs normally leaving the composition it'd generate a new key and thus clear any saved state it was referring to?
a
Glad you were able to resolve it! The composite key will remain the same in either case - it has more to do with timing.
rememberSaveable
internally registers a provider to the
LocalSaveableStateRegistry
. When
rememberSaveable
leaves composition, it unregisters that provider. So from the perspective of
rememberSaveable
, it doesn't care about why it is leaving composition - it just registers and unregisters based on if it is in composition. The resulting behavior is based on when
LocalSaveableStateRegistry
queries all of its providers to gather up saved instance state. When recreation happens, that query will happen before the providers get unregistered. When it is an `if`/`else` swapping whether
rememberSaveable
is in composition, there's no query to save the instance state, so it disappears.
j
Ah I've set some breakpoints in
rememberSaveable
and I think I'm understanding better from your explanation. When I dug into it I had the "oh duh" moment because when switching between an if/else normally, the`rememberSaveable` isn't going to save anything off, it's only going to save something off from a config change or basically
onSaveInstanceState
right? So there is nothing for the registry to restore in the normal if/else, but in the case of the rotation, that registry still had that value and going back into the if makes this "new"
rememberSaveable
consume that restored value
I was then trying to figure out how it knows to distinguish the values between different
rememberSaveable
's in order to put them back into the correct order, and it seems like that key is always the same like you said. I'm guessing this is the important piece to tying them back to the same rememberSaveable calls? It uses the order of the calls to essentially make a queue for each rememberSaveable to call.
a
Right, and then there's one more layer which makes everything a whole tree structure in the general case. https://developer.android.com/reference/kotlin/androidx/compose/runtime/saveable/SaveableStateHolder allows persisting instance state manually for keyed content, which you want for the navigation use case. Lazy layouts also support that as well for items
j
That's really neat. Thanks again for helping me understand it better! Having the ability to dive right into the compose source code makes it so much easier to understand.
1
a
cs.android.com is the best thing ever