Skip to content

Commit 8cb0a82

Browse files
authored
Merge pull request #1213 from square/ray/lifecycle-based-ComposeView-strategy
Uses `DisposeOnViewTreeLifecycleDestroyed` for `ComposeView`
2 parents 040ece4 + 6c798f8 commit 8cb0a82

File tree

2 files changed

+182
-0
lines changed

2 files changed

+182
-0
lines changed

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

Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package com.squareup.workflow1.ui.compose
33
import android.content.Context
44
import android.view.View
55
import android.view.ViewGroup
6+
import android.widget.FrameLayout
67
import androidx.activity.ComponentDialog
78
import androidx.compose.foundation.clickable
89
import androidx.compose.foundation.text.BasicText
@@ -30,10 +31,13 @@ import com.squareup.workflow1.ui.Compatible
3031
import com.squareup.workflow1.ui.NamedScreen
3132
import com.squareup.workflow1.ui.Screen
3233
import com.squareup.workflow1.ui.ScreenViewFactory
34+
import com.squareup.workflow1.ui.ScreenViewFactory.Companion.fromCode
3335
import com.squareup.workflow1.ui.ScreenViewHolder
3436
import com.squareup.workflow1.ui.ViewEnvironment
3537
import com.squareup.workflow1.ui.ViewRegistry
3638
import com.squareup.workflow1.ui.WorkflowUiExperimentalApi
39+
import com.squareup.workflow1.ui.WorkflowViewStub
40+
import com.squareup.workflow1.ui.Wrapper
3741
import com.squareup.workflow1.ui.internal.test.IdleAfterTestRule
3842
import com.squareup.workflow1.ui.internal.test.IdlingDispatcherRule
3943
import com.squareup.workflow1.ui.internal.test.WorkflowUiTestActivity
@@ -651,6 +655,162 @@ internal class ComposeViewTreeIntegrationTest {
651655
.assertIsDisplayed()
652656
}
653657

658+
@Test fun composition_handles_overlay_reordering() {
659+
val composeA: Screen = VanillaComposeRendering(
660+
compatibilityKey = "0",
661+
) {
662+
var counter by rememberSaveable { mutableStateOf(0) }
663+
BasicText(
664+
"Counter: $counter",
665+
Modifier
666+
.clickable { counter++ }
667+
.testTag(CounterTag)
668+
)
669+
}
670+
671+
val composeB: Screen = VanillaComposeRendering(
672+
compatibilityKey = "1",
673+
) {
674+
var counter by rememberSaveable { mutableStateOf(0) }
675+
BasicText(
676+
"Counter2: $counter",
677+
Modifier
678+
.clickable { counter++ }
679+
.testTag(CounterTag2)
680+
)
681+
}
682+
683+
scenario.onActivity {
684+
it.setRendering(
685+
BodyAndOverlaysScreen(
686+
EmptyRendering,
687+
listOf(
688+
TestOverlay(composeA),
689+
TestOverlay(composeB),
690+
// When we move this to the front, both of the other previously-upstream-
691+
// now-downstream dialogs will be dismissed and re-shown.
692+
TestOverlay(EmptyRendering)
693+
)
694+
)
695+
)
696+
}
697+
698+
composeRule.onNodeWithTag(CounterTag)
699+
.assertTextEquals("Counter: 0")
700+
.performClick()
701+
.assertTextEquals("Counter: 1")
702+
703+
composeRule.onNodeWithTag(CounterTag2)
704+
.assertTextEquals("Counter2: 0")
705+
.performClick()
706+
.assertTextEquals("Counter2: 1")
707+
708+
// Reorder the overlays, dialogs will be dismissed and re-shown to preserve order.
709+
710+
scenario.onActivity {
711+
it.setRendering(
712+
BodyAndOverlaysScreen(
713+
EmptyRendering,
714+
listOf(
715+
TestOverlay(EmptyRendering),
716+
TestOverlay(composeB),
717+
TestOverlay(composeA),
718+
)
719+
)
720+
)
721+
}
722+
723+
// Are they still responsive?
724+
725+
composeRule.onNodeWithTag(CounterTag)
726+
.assertTextEquals("Counter: 1")
727+
.performClick()
728+
.assertTextEquals("Counter: 2")
729+
730+
composeRule.onNodeWithTag(CounterTag2)
731+
.assertTextEquals("Counter2: 1")
732+
.performClick()
733+
.assertTextEquals("Counter2: 2")
734+
}
735+
736+
@Test fun composition_under_view_stub_handles_overlay_reordering() {
737+
val composeA: Screen = VanillaComposeRendering(
738+
compatibilityKey = "0",
739+
) {
740+
var counter by rememberSaveable { mutableStateOf(0) }
741+
BasicText(
742+
"Counter: $counter",
743+
Modifier
744+
.clickable { counter++ }
745+
.testTag(CounterTag)
746+
)
747+
}
748+
749+
val composeB: Screen = VanillaComposeRendering(
750+
compatibilityKey = "1",
751+
) {
752+
var counter by rememberSaveable { mutableStateOf(0) }
753+
BasicText(
754+
"Counter2: $counter",
755+
Modifier
756+
.clickable { counter++ }
757+
.testTag(CounterTag2)
758+
)
759+
}
760+
761+
scenario.onActivity {
762+
it.setRendering(
763+
BodyAndOverlaysScreen(
764+
EmptyRendering,
765+
listOf(
766+
TestOverlay(ViewStubWrapper(composeA)),
767+
TestOverlay(ViewStubWrapper(composeB)),
768+
// When we move this to the front, both of the other previously-upstream-
769+
// now-downstream dialogs will be dismissed and re-shown.
770+
TestOverlay(EmptyRendering)
771+
)
772+
)
773+
)
774+
}
775+
776+
composeRule.onNodeWithTag(CounterTag)
777+
.assertTextEquals("Counter: 0")
778+
.performClick()
779+
.assertTextEquals("Counter: 1")
780+
781+
composeRule.onNodeWithTag(CounterTag2)
782+
.assertTextEquals("Counter2: 0")
783+
.performClick()
784+
.assertTextEquals("Counter2: 1")
785+
786+
// Reorder the overlays, dialogs will be dismissed and re-shown to preserve order.
787+
788+
scenario.onActivity {
789+
it.setRendering(
790+
BodyAndOverlaysScreen(
791+
EmptyRendering,
792+
listOf(
793+
TestOverlay(EmptyRendering),
794+
TestOverlay(ViewStubWrapper(composeB)),
795+
TestOverlay(ViewStubWrapper(composeA)),
796+
)
797+
)
798+
)
799+
}
800+
801+
// Are they still responsive?
802+
803+
composeRule.onNodeWithTag(CounterTag)
804+
.assertTextEquals("Counter: 1")
805+
.performClick()
806+
.assertTextEquals("Counter: 2")
807+
808+
composeRule.onNodeWithTag(CounterTag2)
809+
.assertTextEquals("Counter2: 1")
810+
.performClick()
811+
.assertTextEquals("Counter2: 2")
812+
}
813+
654814
private fun WorkflowUiTestActivity.setBackstack(vararg backstack: Screen) {
655815
setRendering(
656816
BackStackScreen.fromList(listOf<AndroidScreen<*>>(EmptyRendering) + backstack.asList())
@@ -668,6 +828,26 @@ internal class ComposeViewTreeIntegrationTest {
668828
}
669829
}
670830

831+
data class ViewStubWrapper<C : Screen>(
832+
override val content: C
833+
) : Screen, Wrapper<Screen, C>, AndroidScreen<ViewStubWrapper<C>> {
834+
override fun <D : Screen> map(transform: (C) -> D) = ViewStubWrapper(transform(content))
835+
836+
override val viewFactory: ScreenViewFactory<ViewStubWrapper<C>> =
837+
fromCode { _, initialEnvironment, context, _ ->
838+
val stub = WorkflowViewStub(context)
839+
840+
FrameLayout(context)
841+
.apply {
842+
this.addView(stub)
843+
}.let {
844+
ScreenViewHolder(initialEnvironment, it) { r, e ->
845+
stub.show(r.content, e)
846+
}
847+
}
848+
}
849+
}
850+
671851
/**
672852
* This is our own custom lovingly handcrafted implementation that creates [ComposeView]
673853
* itself, bypassing [ScreenComposableFactory] entirely. Allows us to mess with alternative

workflow-ui/compose/src/main/java/com/squareup/workflow1/ui/compose/ScreenComposableFactory.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import androidx.compose.runtime.CompositionLocalProvider
1010
import androidx.compose.runtime.remember
1111
import androidx.compose.ui.platform.ComposeView
1212
import androidx.compose.ui.platform.LocalLifecycleOwner
13+
import androidx.compose.ui.platform.ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed
1314
import androidx.compose.ui.viewinterop.AndroidView
1415
import androidx.lifecycle.setViewTreeLifecycleOwner
1516
import com.squareup.workflow1.ui.Screen
@@ -125,6 +126,7 @@ public fun <ScreenT : Screen> ScreenComposableFactory<ScreenT>.asViewFactory():
125126
container: ViewGroup?
126127
): ScreenViewHolder<ScreenT> {
127128
val view = ComposeView(context)
129+
view.setViewCompositionStrategy(DisposeOnViewTreeLifecycleDestroyed)
128130
return ScreenViewHolder(initialEnvironment, view) { newRendering, environment ->
129131

130132
// Update the state whenever a new rendering is emitted.

0 commit comments

Comments
 (0)