Skip to content

Commit d2ff0ad

Browse files
ModalContainer provides SavedStateRegistries to each of its layers.
Without this `BackStackContainer` is the only thing that can host `ComposeView` in `Dialog` windows. Note that we delete `BackStackStateKey`, which was a wart to get around the absence of this work. This fixes #469, and also fixes #470. Co-authored-by: Ray Ryan <[email protected]>
1 parent 12d54f8 commit d2ff0ad

File tree

13 files changed

+334
-200
lines changed

13 files changed

+334
-200
lines changed

samples/containers/android/src/main/java/com/squareup/sample/container/overviewdetail/OverviewDetailContainer.kt

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,12 @@ import com.squareup.sample.container.overviewdetail.OverviewDetailConfig.Detail
88
import com.squareup.sample.container.overviewdetail.OverviewDetailConfig.Overview
99
import com.squareup.sample.container.overviewdetail.OverviewDetailConfig.Single
1010
import com.squareup.workflow1.ui.LayoutRunner
11+
import com.squareup.workflow1.ui.Named
1112
import com.squareup.workflow1.ui.ViewEnvironment
1213
import com.squareup.workflow1.ui.ViewFactory
1314
import com.squareup.workflow1.ui.WorkflowUiExperimentalApi
1415
import com.squareup.workflow1.ui.WorkflowViewStub
1516
import com.squareup.workflow1.ui.backstack.BackStackScreen
16-
import com.squareup.workflow1.ui.backstack.withBackStackStateKeyPrefix
1717

1818
/**
1919
* Displays [OverviewDetailScreen] renderings in either split pane or single pane
@@ -53,11 +53,8 @@ class OverviewDetailContainer(view: View) : LayoutRunner<OverviewDetailScreen> {
5353
if (rendering.detailRendering == null && rendering.selectDefault != null) {
5454
rendering.selectDefault!!.invoke()
5555
} else {
56-
// Since we have two sibling backstacks, we need to give them each different
57-
// SavedStateRegistry key prefixes.
58-
val overviewViewEnvironment = viewEnvironment
59-
.withBackStackStateKeyPrefix(OverviewBackStackKey) + (OverviewDetailConfig to Overview)
60-
overviewStub!!.update(rendering.overviewRendering, overviewViewEnvironment)
56+
val overviewViewEnvironment = viewEnvironment + (OverviewDetailConfig to Overview)
57+
overviewStub!!.update(Named(rendering.overviewRendering, "overview"), overviewViewEnvironment)
6158
rendering.detailRendering
6259
?.let { detail ->
6360
detailStub!!.actual.visibility = VISIBLE
@@ -87,8 +84,5 @@ class OverviewDetailContainer(view: View) : LayoutRunner<OverviewDetailScreen> {
8784
companion object : ViewFactory<OverviewDetailScreen> by LayoutRunner.bind(
8885
layoutId = R.layout.overview_detail,
8986
constructor = ::OverviewDetailContainer
90-
) {
91-
private const val OverviewBackStackKey = "overview"
92-
private const val DetailBackStackKey = "detail"
93-
}
87+
)
9488
}

workflow-ui/compose/src/androidTest/java/com/squareup/workflow1/ui/compose/ComposeViewTreeIntegrationTest.kt

Lines changed: 123 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@ import androidx.compose.ui.test.performClick
2626
import com.google.common.truth.Truth.assertThat
2727
import com.squareup.workflow1.ui.AndroidViewRendering
2828
import com.squareup.workflow1.ui.Compatible
29-
import com.squareup.workflow1.ui.Named
3029
import com.squareup.workflow1.ui.ViewEnvironment
3130
import com.squareup.workflow1.ui.ViewFactory
3231
import com.squareup.workflow1.ui.ViewRegistry
@@ -394,7 +393,7 @@ internal class ComposeViewTreeIntegrationTest {
394393
}
395394

396395
@Test fun composition_is_restored_in_multiple_modals_after_config_change() {
397-
val firstScreen = ComposeRendering(compatibilityKey = "first") {
396+
val firstScreen = ComposeRendering(compatibilityKey = "key") {
398397
var counter by rememberSaveable { mutableStateOf(0) }
399398
BasicText(
400399
"Counter: $counter",
@@ -403,7 +402,9 @@ internal class ComposeViewTreeIntegrationTest {
403402
.testTag(CounterTag)
404403
)
405404
}
406-
val secondScreen = ComposeRendering(compatibilityKey = "second") {
405+
// Use the same compatibility key – these screens are in different modals, so they won't
406+
// conflict.
407+
val secondScreen = ComposeRendering(compatibilityKey = "key") {
407408
var counter by rememberSaveable { mutableStateOf(0) }
408409
BasicText(
409410
"Counter2: $counter",
@@ -412,17 +413,26 @@ internal class ComposeViewTreeIntegrationTest {
412413
.testTag(CounterTag2)
413414
)
414415
}
416+
// Use the same compatibility key – these screens are in different modals, so they won't
417+
// conflict.
418+
val thirdScreen = ComposeRendering(compatibilityKey = "key") {
419+
var counter by rememberSaveable { mutableStateOf(0) }
420+
BasicText(
421+
"Counter3: $counter",
422+
Modifier
423+
.clickable { counter++ }
424+
.testTag(CounterTag3)
425+
)
426+
}
415427

416428
// Show first screen to initialize state.
417429
scenario.onActivity {
418430
it.setRendering(
419431
TestModalScreen(
420432
listOf(
421-
// Name each BackStackScreen to give them unique state registry keys.
422-
// TODO(https://github.com/square/workflow-kotlin/issues/469) Should this naming be
423-
// done automatically in ModalContainer?
424-
Named(BackStackScreen(EmptyRendering, firstScreen), "modal1"),
425-
Named(BackStackScreen(EmptyRendering, secondScreen), "modal2")
433+
firstScreen,
434+
secondScreen,
435+
thirdScreen
426436
)
427437
)
428438
)
@@ -438,13 +448,117 @@ internal class ComposeViewTreeIntegrationTest {
438448
.performClick()
439449
.assertTextEquals("Counter2: 1")
440450

451+
composeRule.onNodeWithTag(CounterTag3)
452+
.assertTextEquals("Counter3: 0")
453+
.performClick()
454+
.assertTextEquals("Counter3: 1")
455+
441456
scenario.recreate()
442457

443458
composeRule.onNodeWithTag(CounterTag)
444459
.assertTextEquals("Counter: 1")
445460

446461
composeRule.onNodeWithTag(CounterTag2)
447462
.assertTextEquals("Counter2: 1")
463+
464+
composeRule.onNodeWithTag(CounterTag3)
465+
.assertTextEquals("Counter3: 1")
466+
}
467+
468+
@Test fun composition_is_restored_in_multiple_modals_backstacks_after_config_change() {
469+
fun createRendering(
470+
layer: Int,
471+
screen: Int
472+
) = ComposeRendering(
473+
// Use the same compatibility key across layers – these screens are in different modals, so
474+
// they won't conflict.
475+
compatibilityKey = screen.toString()
476+
) {
477+
var counter by rememberSaveable { mutableStateOf(0) }
478+
BasicText(
479+
"Counter[$layer][$screen]: $counter",
480+
Modifier
481+
.clickable { counter++ }
482+
.testTag("[$layer][$screen]")
483+
)
484+
}
485+
486+
val layer0Screen0 = createRendering(0, 0)
487+
val layer0Screen1 = createRendering(0, 1)
488+
val layer1Screen0 = createRendering(1, 0)
489+
val layer1Screen1 = createRendering(1, 1)
490+
491+
// Show first screen to initialize state.
492+
scenario.onActivity {
493+
it.setRendering(
494+
TestModalScreen(
495+
listOf(
496+
BackStackScreen(EmptyRendering, layer0Screen0),
497+
BackStackScreen(EmptyRendering, layer1Screen0)
498+
)
499+
)
500+
)
501+
}
502+
503+
composeRule.onNodeWithText("Counter[0][0]: 0")
504+
.assertIsDisplayed()
505+
.performClick()
506+
.assertTextEquals("Counter[0][0]: 1")
507+
composeRule.onNodeWithText("Counter[1][0]: 0")
508+
.assertIsDisplayed()
509+
.performClick()
510+
.assertTextEquals("Counter[1][0]: 1")
511+
512+
// Push some screens onto the backstack.
513+
scenario.onActivity {
514+
it.setRendering(
515+
TestModalScreen(
516+
listOf(
517+
BackStackScreen(EmptyRendering, layer0Screen0, layer0Screen1),
518+
BackStackScreen(EmptyRendering, layer1Screen0, layer1Screen1)
519+
)
520+
)
521+
)
522+
}
523+
524+
composeRule.onNodeWithText("Counter[0][0]", substring = true)
525+
.assertDoesNotExist()
526+
composeRule.onNodeWithText("Counter[1][0]", substring = true)
527+
.assertDoesNotExist()
528+
composeRule.onNodeWithText("Counter[0][1]: 0")
529+
.assertIsDisplayed()
530+
.performClick()
531+
.assertTextEquals("Counter[0][1]: 1")
532+
composeRule.onNodeWithText("Counter[1][1]: 0")
533+
.assertIsDisplayed()
534+
.performClick()
535+
.assertTextEquals("Counter[1][1]: 1")
536+
537+
// Simulate config change.
538+
scenario.recreate()
539+
540+
// Check that the last-shown screens were restored.
541+
composeRule.onNodeWithText("Counter[0][1]: 1")
542+
.assertIsDisplayed()
543+
composeRule.onNodeWithText("Counter[1][1]: 1")
544+
.assertIsDisplayed()
545+
546+
// Pop both backstacks and check that screens were restored.
547+
scenario.onActivity {
548+
it.setRendering(
549+
TestModalScreen(
550+
listOf(
551+
BackStackScreen(EmptyRendering, layer0Screen0),
552+
BackStackScreen(EmptyRendering, layer1Screen0)
553+
)
554+
)
555+
)
556+
}
557+
558+
composeRule.onNodeWithText("Counter[0][0]: 1")
559+
.assertIsDisplayed()
560+
composeRule.onNodeWithText("Counter[1][0]: 1")
561+
.assertIsDisplayed()
448562
}
449563

450564
private fun WorkflowUiTestActivity.setBackstack(vararg backstack: ComposeRendering) {
@@ -495,5 +609,6 @@ internal class ComposeViewTreeIntegrationTest {
495609

496610
const val CounterTag = "counter"
497611
const val CounterTag2 = "counter2"
612+
const val CounterTag3 = "counter3"
498613
}
499614
}

workflow-ui/container-android/api/container-android.api

Lines changed: 6 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -33,33 +33,6 @@ public final class com/squareup/workflow1/ui/backstack/BackStackContainer$Compan
3333
public fun getType ()Lkotlin/reflect/KClass;
3434
}
3535

36-
public final class com/squareup/workflow1/ui/backstack/BackStackStateKeyKt {
37-
public static final fun getGetBackStackStateKeyPrefix (Lcom/squareup/workflow1/ui/ViewEnvironment;)Ljava/lang/String;
38-
public static final fun withBackStackStateKeyPrefix (Lcom/squareup/workflow1/ui/ViewEnvironment;Ljava/lang/String;)Lcom/squareup/workflow1/ui/ViewEnvironment;
39-
}
40-
41-
public final class com/squareup/workflow1/ui/backstack/KeyedStateRegistryOwner : androidx/lifecycle/LifecycleOwner, androidx/savedstate/SavedStateRegistryOwner {
42-
public static final field Companion Lcom/squareup/workflow1/ui/backstack/KeyedStateRegistryOwner$Companion;
43-
public synthetic fun <init> (Ljava/lang/String;Landroidx/lifecycle/LifecycleOwner;Lkotlin/jvm/internal/DefaultConstructorMarker;)V
44-
public final fun getController ()Landroidx/savedstate/SavedStateRegistryController;
45-
public final fun getKey ()Ljava/lang/String;
46-
public fun getLifecycle ()Landroidx/lifecycle/Lifecycle;
47-
public fun getSavedStateRegistry ()Landroidx/savedstate/SavedStateRegistry;
48-
}
49-
50-
public final class com/squareup/workflow1/ui/backstack/KeyedStateRegistryOwner$Companion {
51-
public final fun installAsSavedStateRegistryOwnerOn (Landroid/view/View;Ljava/lang/String;)Lcom/squareup/workflow1/ui/backstack/KeyedStateRegistryOwner;
52-
}
53-
54-
public final class com/squareup/workflow1/ui/backstack/StateRegistryAggregator {
55-
public fun <init> (Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;)V
56-
public final fun attachToParentRegistry (Ljava/lang/String;Landroidx/savedstate/SavedStateRegistryOwner;)V
57-
public final fun detachFromParentRegistry ()V
58-
public final fun pruneKeys (Ljava/util/Collection;)V
59-
public final fun restoreRegistryControllerIfReady (Ljava/lang/String;Landroidx/savedstate/SavedStateRegistryController;)V
60-
public final fun saveRegistryController (Ljava/lang/String;Landroidx/savedstate/SavedStateRegistryController;)V
61-
}
62-
6336
public final class com/squareup/workflow1/ui/backstack/ViewStateCache : android/os/Parcelable {
6437
public static final field CREATOR Lcom/squareup/workflow1/ui/backstack/ViewStateCache$CREATOR;
6538
public fun <init> ()V
@@ -148,31 +121,30 @@ public abstract class com/squareup/workflow1/ui/modal/ModalContainer : android/w
148121
public fun <init> (Landroid/content/Context;Landroid/util/AttributeSet;II)V
149122
public synthetic fun <init> (Landroid/content/Context;Landroid/util/AttributeSet;IIILkotlin/jvm/internal/DefaultConstructorMarker;)V
150123
protected abstract fun buildDialog (Ljava/lang/Object;Lcom/squareup/workflow1/ui/ViewEnvironment;)Lcom/squareup/workflow1/ui/modal/ModalContainer$DialogRef;
124+
protected fun onAttachedToWindow ()V
125+
protected fun onDetachedFromWindow ()V
151126
protected fun onRestoreInstanceState (Landroid/os/Parcelable;)V
152127
protected fun onSaveInstanceState ()Landroid/os/Parcelable;
153128
protected final fun update (Lcom/squareup/workflow1/ui/modal/HasModals;Lcom/squareup/workflow1/ui/ViewEnvironment;)V
154129
protected abstract fun updateDialog (Lcom/squareup/workflow1/ui/modal/ModalContainer$DialogRef;)V
155130
}
156131

157132
protected final class com/squareup/workflow1/ui/modal/ModalContainer$DialogRef {
133+
public field stateRegistryOwner Lcom/squareup/workflow1/ui/androidx/KeyedStateRegistryOwner;
158134
public fun <init> (Ljava/lang/Object;Lcom/squareup/workflow1/ui/ViewEnvironment;Landroid/app/Dialog;Ljava/lang/Object;)V
159135
public synthetic fun <init> (Ljava/lang/Object;Lcom/squareup/workflow1/ui/ViewEnvironment;Landroid/app/Dialog;Ljava/lang/Object;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
160-
public final fun component1 ()Ljava/lang/Object;
161-
public final fun component2 ()Lcom/squareup/workflow1/ui/ViewEnvironment;
162-
public final fun component3 ()Landroid/app/Dialog;
163-
public final fun component4 ()Ljava/lang/Object;
164-
public final fun copy (Ljava/lang/Object;Lcom/squareup/workflow1/ui/ViewEnvironment;Landroid/app/Dialog;Ljava/lang/Object;)Lcom/squareup/workflow1/ui/modal/ModalContainer$DialogRef;
165-
public static synthetic fun copy$default (Lcom/squareup/workflow1/ui/modal/ModalContainer$DialogRef;Ljava/lang/Object;Lcom/squareup/workflow1/ui/ViewEnvironment;Landroid/app/Dialog;Ljava/lang/Object;ILjava/lang/Object;)Lcom/squareup/workflow1/ui/modal/ModalContainer$DialogRef;
136+
public final fun copy (Ljava/lang/Object;Lcom/squareup/workflow1/ui/ViewEnvironment;)Lcom/squareup/workflow1/ui/modal/ModalContainer$DialogRef;
166137
public final fun dismiss$wf1_container_android ()V
167138
public fun equals (Ljava/lang/Object;)Z
168139
public final fun getDialog ()Landroid/app/Dialog;
169140
public final fun getExtra ()Ljava/lang/Object;
170141
public final fun getModalRendering ()Ljava/lang/Object;
142+
public final fun getStateRegistryOwner$wf1_container_android ()Lcom/squareup/workflow1/ui/androidx/KeyedStateRegistryOwner;
171143
public final fun getViewEnvironment ()Lcom/squareup/workflow1/ui/ViewEnvironment;
172144
public fun hashCode ()I
173145
public final fun restore$wf1_container_android (Lcom/squareup/workflow1/ui/modal/ModalContainer$KeyAndBundle;)V
174146
public final fun save$wf1_container_android ()Lcom/squareup/workflow1/ui/modal/ModalContainer$KeyAndBundle;
175-
public fun toString ()Ljava/lang/String;
147+
public final fun setStateRegistryOwner$wf1_container_android (Lcom/squareup/workflow1/ui/androidx/KeyedStateRegistryOwner;)V
176148
}
177149

178150
public final class com/squareup/workflow1/ui/modal/ModalContainer$KeyAndBundle : android/os/Parcelable {

workflow-ui/container-android/src/androidTest/java/com/squareup/workflow1/ui/backstack/test/ViewStateCacheTest.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@ import com.google.common.truth.Truth.assertThat
1010
import com.squareup.workflow1.ui.Named
1111
import com.squareup.workflow1.ui.ViewEnvironment
1212
import com.squareup.workflow1.ui.WorkflowUiExperimentalApi
13+
import com.squareup.workflow1.ui.androidx.KeyedStateRegistryOwner
1314
import com.squareup.workflow1.ui.androidx.WorkflowLifecycleOwner
14-
import com.squareup.workflow1.ui.backstack.KeyedStateRegistryOwner
1515
import com.squareup.workflow1.ui.backstack.ViewStateCache
1616
import com.squareup.workflow1.ui.backstack.ViewStateFrame
1717
import com.squareup.workflow1.ui.backstack.test.fixtures.ViewStateTestView

0 commit comments

Comments
 (0)