-
-
Notifications
You must be signed in to change notification settings - Fork 23
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Start using UiSavedStateRegistry to preserve screen "view" state.
Currently screens' states are saved as long as the Backstack is in the composition by being kept entirely alive. This is not efficient, it's equivalent to keeping all views in the backstack attached at all times. Ideally we would only keep screens in the composition while they are actually visible, and remove all hidden screens as soon as possible. Unfortunately, until dev08, the only way to preserve view state was to keep these screens composed. Dev08 introduces `rememberSavedInstanceState` and `savedInstanceState` composables. They mirror `remember` and `state`, but will restore their values from a `UiSavedStateRegistry` when they are first composed. This is the standard API for saving view state, much like `onSavedInstanceState` in the legacy framework, and so the backstack should support it. This change does a few things: - When a screen is not longer visible, remove it from the composition. Note that the `ScreenWrapper` remains in the composition as long as the screen's key is in the active backstack, but when a screen stops being visible, we stop calling the `drawScreen` function for it. Note that this means we no longer need to set the "hidden" semantics property on screens that aren't visible. - The `ScreenWrapper` holds a map of saved state values. This map, in turn, gets saved and restored to the parent's `UiSavedStateRegistry`, so it can survive activity recreations etc. - Each screen is wrapped with its own `UiSavedStateRegistry` that is scoped to just that screen. This registry is asked to save values to the above map when the screen is going to be hidden, and is re-created to restore from the cached values when it is shown. The keys to making this work are: - Scoping `ScreenWrapper`s to the presence of a screen in the backstack, even though the screen's actual composables are only alive when the screen is visible. - The mechanism that the saved state machinery uses to key values is derived from the positional memoization keys. The only way this works is because those keys remain static even when a composable is removed and re-added later. Because that happens, and because those keys are globally unique (at least within the composition), the individual screen registries don't need to do any scoping of their keys, and we don't need to worry about somehow turning the screen keys (`T`) into state keys (`String`). The one thing I haven't verified is if these keys stay constant even if items in the hidden backstack are reordered, but I expect they will since `@Pivotal` makes use of this same keying infrastructure.
- Loading branch information
1 parent
9b01032
commit 0604179
Showing
7 changed files
with
182 additions
and
41 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
69 changes: 69 additions & 0 deletions
69
compose-backstack/src/main/java/com/zachklipp/compose/backstack/ChildSavedStateRegistry.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,69 @@ | ||
package com.zachklipp.compose.backstack | ||
|
||
import androidx.compose.Composable | ||
import androidx.compose.remember | ||
import androidx.ui.savedinstancestate.UiSavedStateRegistry | ||
import androidx.ui.savedinstancestate.UiSavedStateRegistryAmbient | ||
import androidx.ui.savedinstancestate.rememberSavedInstanceState | ||
|
||
/** | ||
* Returns a [UiSavedStateRegistry] that will automatically save values from all its registered | ||
* providers whenever [childWillBeComposed] transitions from true to false, and make those values available | ||
* to be restored when [childWillBeComposed] transitions from false to true. | ||
*/ | ||
@Composable | ||
@Suppress("RemoveExplicitTypeArguments") | ||
fun ChildSavedStateRegistry(childWillBeComposed: Boolean): UiSavedStateRegistry { | ||
val parentRegistry = UiSavedStateRegistryAmbient.current | ||
|
||
// This map holds all the savedInstanceState for this screen as long as it exists | ||
// in the backstack. When the screen is hidden, we will cache its state providers | ||
// into this map before removing it from the composition. This cache will in turn | ||
// be persisted into and restored from the parent UiSavedStateRegistry. | ||
val values = rememberSavedInstanceState<MutableMap<String, Any>> { mutableMapOf() } | ||
val holder = remember { | ||
// If there's no registry available, then we won't be restored anyway so there are no | ||
// serializability restrictions on saved values. | ||
val canBeSaved: (Any) -> Boolean = parentRegistry?.let { it::canBeSaved } ?: { true } | ||
SavedStateHolder(canBeSaved, values) | ||
} | ||
holder.setScreenVisibility(childWillBeComposed) | ||
return holder.registry | ||
} | ||
|
||
internal class SavedStateHolder( | ||
private val canBeSaved: (Any) -> Boolean, | ||
private var values: Map<String, Any> | ||
) { | ||
var registry: UiSavedStateRegistry = createRegistry() | ||
private set | ||
private var isScreenVisible = false | ||
|
||
/** | ||
* Tracks the visibility of the screen this class holds the state for and returns either the | ||
* [UiSavedStateRegistry] if visible, or null if not visible. | ||
* | ||
* When [isVisible] transitions from false to true, a new registry will be created that can will | ||
* restore from previously-saved values. | ||
* | ||
* When [isVisible] transitions from true to false, the existing registry will be used to save | ||
* all values. | ||
*/ | ||
fun setScreenVisibility(isVisible: Boolean) { | ||
if (isVisible == this.isScreenVisible) return | ||
this.isScreenVisible = isVisible | ||
|
||
if (!isVisible) { | ||
// This will automatically preserve any values that were passed into the factory | ||
// function but not consumed. | ||
values = registry.performSave() | ||
} else { | ||
// Recreate the registry so the most recently-saved values will be used to restore. | ||
// The UiSavedStateRegistry function makes a defensive copy of the passed-in map, so | ||
// it needs to be recreated on every restoration. | ||
registry = createRegistry() | ||
} | ||
} | ||
|
||
private fun createRegistry() = UiSavedStateRegistry(values, canBeSaved) | ||
} |
56 changes: 56 additions & 0 deletions
56
compose-backstack/src/test/java/com/zachklipp/compose/backstack/SavedStateHolderTest.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,56 @@ | ||
package com.zachklipp.compose.backstack | ||
|
||
import com.google.common.truth.Truth.assertThat | ||
import org.junit.Test | ||
|
||
class SavedStateHolderTest { | ||
|
||
@Test | ||
fun `saves and restores`() { | ||
val holder = SavedStateHolder(canBeSaved = { true }, values = mutableMapOf()) | ||
|
||
holder.setScreenVisibility(true) | ||
holder.registry.registerProvider("key") { "value" } | ||
holder.setScreenVisibility(false) | ||
holder.registry.unregisterProvider("key") | ||
holder.setScreenVisibility(true) | ||
|
||
assertThat(holder.registry.consumeRestored("key")).isEqualTo("value") | ||
} | ||
|
||
@Test | ||
fun `restores from initial values`() { | ||
val holder = | ||
SavedStateHolder(canBeSaved = { true }, values = mutableMapOf("key" to "value")) | ||
|
||
holder.setScreenVisibility(true) | ||
|
||
assertThat(holder.registry.consumeRestored("key")).isEqualTo("value") | ||
} | ||
|
||
@Test | ||
fun `doesn't save unregistered providers`() { | ||
val holder = SavedStateHolder(canBeSaved = { true }, values = mutableMapOf()) | ||
|
||
holder.setScreenVisibility(true) | ||
holder.registry.registerProvider("key") { "value" } | ||
holder.registry.unregisterProvider("key") | ||
holder.setScreenVisibility(false) | ||
holder.setScreenVisibility(true) | ||
|
||
assertThat(holder.registry.consumeRestored("key")).isNull() | ||
} | ||
|
||
@Test | ||
fun `preserves unrestored values from previous save`() { | ||
val holder = | ||
SavedStateHolder(canBeSaved = { true }, values = mutableMapOf("old key" to "old value")) | ||
|
||
holder.setScreenVisibility(true) | ||
// Performs the save without having consumed "old key". | ||
holder.setScreenVisibility(false) | ||
holder.setScreenVisibility(true) | ||
|
||
assertThat(holder.registry.consumeRestored("old key")).isEqualTo("old value") | ||
} | ||
} |