Fix the issue that pane content states are not saved
The content inside a pane has to be wrapped inside SaveableStateHolder
as they can be removed from the layout hierarchy when navigation inside
a pane scaffold happens. Introduce this as a default behavior of
AnimatedPane.
Test: instrumentation test
Bug: 343845141
Bug: 374064147
Change-Id: I98399f90e102a266b24c502bafa9b19f37e6bc8b
diff --git a/compose/material3/adaptive/adaptive-layout/src/androidInstrumentedTest/kotlin/androidx/compose/material3/adaptive/layout/ThreePaneScaffoldTest.kt b/compose/material3/adaptive/adaptive-layout/src/androidInstrumentedTest/kotlin/androidx/compose/material3/adaptive/layout/ThreePaneScaffoldTest.kt
index 3677863..5abcd21 100644
--- a/compose/material3/adaptive/adaptive-layout/src/androidInstrumentedTest/kotlin/androidx/compose/material3/adaptive/layout/ThreePaneScaffoldTest.kt
+++ b/compose/material3/adaptive/adaptive-layout/src/androidInstrumentedTest/kotlin/androidx/compose/material3/adaptive/layout/ThreePaneScaffoldTest.kt
@@ -23,11 +23,14 @@
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.test.junit4.StateRestorationTester
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.unit.dp
@@ -399,6 +402,62 @@
}
}
}
+
+ @Test
+ fun threePaneScaffold_afterPaneSwitching_paneStatesAreSaved() {
+ val restorationTester = StateRestorationTester(rule)
+ val scaffoldValueSecondaryShown =
+ ThreePaneScaffoldValue(
+ primary = PaneAdaptedValue.Expanded,
+ secondary = PaneAdaptedValue.Expanded,
+ tertiary = PaneAdaptedValue.Hidden
+ )
+ val scaffoldValueSecondaryHidden =
+ ThreePaneScaffoldValue(
+ primary = PaneAdaptedValue.Expanded,
+ secondary = PaneAdaptedValue.Hidden,
+ tertiary = PaneAdaptedValue.Hidden
+ )
+
+ var increment = 0
+ var numberOnSecondaryPane = -1
+ var restorableNumberOnSecondaryPane = -1
+ var testScaffoldValue by mutableStateOf(scaffoldValueSecondaryShown)
+
+ restorationTester.setContent {
+ SampleThreePaneScaffold(
+ scaffoldDirective = MockScaffoldDirective,
+ scaffoldValue = testScaffoldValue,
+ paneOrder = ListDetailPaneScaffoldDefaults.PaneOrder,
+ secondaryContent = {
+ numberOnSecondaryPane = remember { increment++ }
+ restorableNumberOnSecondaryPane = rememberSaveable { increment++ }
+ }
+ )
+ }
+
+ rule.runOnIdle {
+ assertThat(numberOnSecondaryPane).isEqualTo(0)
+ assertThat(restorableNumberOnSecondaryPane).isEqualTo(1)
+ testScaffoldValue = scaffoldValueSecondaryHidden
+ }
+
+ // wait for the screen switch to apply
+ rule.runOnIdle {
+ numberOnSecondaryPane = -1
+ restorableNumberOnSecondaryPane = -1
+ }
+
+ restorationTester.emulateSavedInstanceStateRestore()
+
+ // switch back to screen1
+ rule.runOnIdle { testScaffoldValue = scaffoldValueSecondaryShown }
+
+ rule.runOnIdle {
+ assertThat(numberOnSecondaryPane).isEqualTo(2)
+ assertThat(restorableNumberOnSecondaryPane).isEqualTo(1)
+ }
+ }
}
private val MockScaffoldDirective = PaneScaffoldDirective.Default
@@ -434,6 +493,9 @@
paneExpansionDragHandle: (@Composable ThreePaneScaffoldScope.(PaneExpansionState) -> Unit)? =
null,
paneExpansionState: PaneExpansionState = PaneExpansionState(),
+ primaryContent: (@Composable ThreePaneScaffoldScope.() -> Unit) = {},
+ secondaryContent: (@Composable ThreePaneScaffoldScope.() -> Unit) = {},
+ tertiaryContent: (@Composable ThreePaneScaffoldScope.() -> Unit) = {}
) {
ThreePaneScaffold(
modifier = Modifier.fillMaxSize().testTag(ThreePaneScaffoldTestTag),
@@ -447,7 +509,9 @@
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.secondary
- ) {}
+ ) {
+ secondaryContent()
+ }
}
},
tertiaryPane = {
@@ -455,12 +519,16 @@
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.tertiary
- ) {}
+ ) {
+ tertiaryContent()
+ }
}
}
) {
AnimatedPane(modifier = Modifier.testTag(tag = "PrimaryPane")) {
- Surface(modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.primary) {}
+ Surface(modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.primary) {
+ primaryContent()
+ }
}
}
}
diff --git a/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/Pane.kt b/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/Pane.kt
index 49dd66a..3e4c481 100644
--- a/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/Pane.kt
+++ b/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/Pane.kt
@@ -151,7 +151,11 @@
enter = enterTransition,
exit = exitTransition
) {
- AnimatedPaneScope.create(this).content()
+ (scope as ThreePaneScaffoldPaneScopeImpl).saveableStateHolder.SaveableStateProvider(
+ paneRole.toString()
+ ) {
+ AnimatedPaneScope.create(this).content()
+ }
}
}
}
diff --git a/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/PaneScaffold.kt b/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/PaneScaffold.kt
index 0607f9f..e205ada 100644
--- a/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/PaneScaffold.kt
+++ b/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/PaneScaffold.kt
@@ -20,6 +20,7 @@
import androidx.compose.animation.core.Transition
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi
+import androidx.compose.runtime.saveable.SaveableStateHolder
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.LookaheadScope
import androidx.compose.ui.layout.Measurable
@@ -139,7 +140,11 @@
val paneMotion: PaneMotion
}
-internal abstract class PaneScaffoldScopeImpl : PaneScaffoldScope {
+@OptIn(ExperimentalMaterial3AdaptiveApi::class)
+internal abstract class PaneScaffoldScopeImpl(
+ // TODO(conradchen): Add it to PaneScaffoldScope API in 1.2
+ val saveableStateHolder: SaveableStateHolder
+) : PaneScaffoldScope {
override fun Modifier.preferredWidth(width: Dp): Modifier {
require(width == Dp.Unspecified || width > 0.dp) { "invalid width" }
return this.then(PreferredWidthElement(width))
diff --git a/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/ThreePaneScaffold.kt b/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/ThreePaneScaffold.kt
index cc543e20..cb5757f 100644
--- a/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/ThreePaneScaffold.kt
+++ b/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/ThreePaneScaffold.kt
@@ -27,6 +27,7 @@
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
+import androidx.compose.runtime.saveable.rememberSaveableStateHolder
import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Modifier
@@ -194,9 +195,13 @@
scaffoldStateTransition = currentTransition
}
+ val stateHolder = rememberSaveableStateHolder()
+
LookaheadScope {
val scaffoldScope =
- remember(currentTransition, this) { ThreePaneScaffoldScopeImpl(transitionScope, this) }
+ remember(currentTransition, this) {
+ ThreePaneScaffoldScopeImpl(transitionScope, this, stateHolder)
+ }
with(LocalThreePaneScaffoldOverride.current) {
ThreePaneScaffoldOverrideContext(
modifier = modifier,
diff --git a/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/ThreePaneScaffoldScope.kt b/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/ThreePaneScaffoldScope.kt
index 0bdd476..90753f6 100644
--- a/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/ThreePaneScaffoldScope.kt
+++ b/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/ThreePaneScaffoldScope.kt
@@ -25,6 +25,7 @@
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
+import androidx.compose.runtime.saveable.SaveableStateHolder
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.LookaheadScope
@@ -46,12 +47,13 @@
@OptIn(ExperimentalMaterial3AdaptiveApi::class)
internal class ThreePaneScaffoldScopeImpl(
transitionScope: PaneScaffoldTransitionScope<ThreePaneScaffoldRole, ThreePaneScaffoldValue>,
- lookaheadScope: LookaheadScope
+ lookaheadScope: LookaheadScope,
+ saveableStateHolder: SaveableStateHolder,
) :
ThreePaneScaffoldScope,
PaneScaffoldTransitionScope<ThreePaneScaffoldRole, ThreePaneScaffoldValue> by transitionScope,
LookaheadScope by lookaheadScope,
- PaneScaffoldScopeImpl() {
+ PaneScaffoldScopeImpl(saveableStateHolder) {
@ExperimentalMaterial3AdaptiveApi
override fun Modifier.paneExpansionDraggable(
@@ -92,6 +94,8 @@
scaffoldScope: ThreePaneScaffoldScope,
) : ThreePaneScaffoldPaneScope, ThreePaneScaffoldScope by scaffoldScope {
override var paneMotion: PaneMotion by mutableStateOf(PaneMotion.ExitToLeft)
+ // TODO(conradchen): Remove this when it goes to public API of PaneScaffoldScope
+ val saveableStateHolder = (scaffoldScope as ThreePaneScaffoldScopeImpl).saveableStateHolder
}
@OptIn(ExperimentalMaterial3AdaptiveApi::class)