Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Uses DisposeOnViewTreeLifecycleDestroyed for ComposeView #1213

Merged
merged 2 commits into from
Jul 31, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package com.squareup.workflow1.ui.compose
import android.content.Context
import android.view.View
import android.view.ViewGroup
import android.widget.FrameLayout
import androidx.activity.ComponentDialog
import androidx.compose.foundation.clickable
import androidx.compose.foundation.text.BasicText
Expand Down Expand Up @@ -30,10 +31,13 @@ import com.squareup.workflow1.ui.Compatible
import com.squareup.workflow1.ui.NamedScreen
import com.squareup.workflow1.ui.Screen
import com.squareup.workflow1.ui.ScreenViewFactory
import com.squareup.workflow1.ui.ScreenViewFactory.Companion.fromCode
import com.squareup.workflow1.ui.ScreenViewHolder
import com.squareup.workflow1.ui.ViewEnvironment
import com.squareup.workflow1.ui.ViewRegistry
import com.squareup.workflow1.ui.WorkflowUiExperimentalApi
import com.squareup.workflow1.ui.WorkflowViewStub
import com.squareup.workflow1.ui.Wrapper
import com.squareup.workflow1.ui.internal.test.IdleAfterTestRule
import com.squareup.workflow1.ui.internal.test.IdlingDispatcherRule
import com.squareup.workflow1.ui.internal.test.WorkflowUiTestActivity
Expand Down Expand Up @@ -651,6 +655,162 @@ internal class ComposeViewTreeIntegrationTest {
.assertIsDisplayed()
}

@Test fun composition_handles_overlay_reordering() {
val composeA: Screen = VanillaComposeRendering(
compatibilityKey = "0",
) {
var counter by rememberSaveable { mutableStateOf(0) }
BasicText(
"Counter: $counter",
Modifier
.clickable { counter++ }
.testTag(CounterTag)
)
}

val composeB: Screen = VanillaComposeRendering(
compatibilityKey = "1",
) {
var counter by rememberSaveable { mutableStateOf(0) }
BasicText(
"Counter2: $counter",
Modifier
.clickable { counter++ }
.testTag(CounterTag2)
)
}

scenario.onActivity {
it.setRendering(
BodyAndOverlaysScreen(
EmptyRendering,
listOf(
TestOverlay(composeA),
TestOverlay(composeB),
// When we move this to the front, both of the other previously-upstream-
// now-downstream dialogs will be dismissed and re-shown.
TestOverlay(EmptyRendering)
)
)
)
}

composeRule.onNodeWithTag(CounterTag)
.assertTextEquals("Counter: 0")
.performClick()
.assertTextEquals("Counter: 1")

composeRule.onNodeWithTag(CounterTag2)
.assertTextEquals("Counter2: 0")
.performClick()
.assertTextEquals("Counter2: 1")

// Reorder the overlays, dialogs will be dismissed and re-shown to preserve order.

scenario.onActivity {
it.setRendering(
BodyAndOverlaysScreen(
EmptyRendering,
listOf(
TestOverlay(EmptyRendering),
TestOverlay(composeB),
TestOverlay(composeA),
)
)
)
}

// Are they still responsive?

composeRule.onNodeWithTag(CounterTag)
.assertTextEquals("Counter: 1")
.performClick()
.assertTextEquals("Counter: 2")

composeRule.onNodeWithTag(CounterTag2)
.assertTextEquals("Counter2: 1")
.performClick()
.assertTextEquals("Counter2: 2")
}

@Test fun composition_under_view_stub_handles_overlay_reordering() {
val composeA: Screen = VanillaComposeRendering(
compatibilityKey = "0",
) {
var counter by rememberSaveable { mutableStateOf(0) }
BasicText(
"Counter: $counter",
Modifier
.clickable { counter++ }
.testTag(CounterTag)
)
}

val composeB: Screen = VanillaComposeRendering(
compatibilityKey = "1",
) {
var counter by rememberSaveable { mutableStateOf(0) }
BasicText(
"Counter2: $counter",
Modifier
.clickable { counter++ }
.testTag(CounterTag2)
)
}

scenario.onActivity {
it.setRendering(
BodyAndOverlaysScreen(
EmptyRendering,
listOf(
TestOverlay(ViewStubWrapper(composeA)),
TestOverlay(ViewStubWrapper(composeB)),
// When we move this to the front, both of the other previously-upstream-
// now-downstream dialogs will be dismissed and re-shown.
TestOverlay(EmptyRendering)
)
)
)
}

composeRule.onNodeWithTag(CounterTag)
.assertTextEquals("Counter: 0")
.performClick()
.assertTextEquals("Counter: 1")

composeRule.onNodeWithTag(CounterTag2)
.assertTextEquals("Counter2: 0")
.performClick()
.assertTextEquals("Counter2: 1")

// Reorder the overlays, dialogs will be dismissed and re-shown to preserve order.

scenario.onActivity {
it.setRendering(
BodyAndOverlaysScreen(
EmptyRendering,
listOf(
TestOverlay(EmptyRendering),
TestOverlay(ViewStubWrapper(composeB)),
TestOverlay(ViewStubWrapper(composeA)),
)
)
)
}

// Are they still responsive?

composeRule.onNodeWithTag(CounterTag)
.assertTextEquals("Counter: 1")
.performClick()
.assertTextEquals("Counter: 2")

composeRule.onNodeWithTag(CounterTag2)
.assertTextEquals("Counter2: 1")
.performClick()
.assertTextEquals("Counter2: 2")
}

private fun WorkflowUiTestActivity.setBackstack(vararg backstack: Screen) {
setRendering(
BackStackScreen.fromList(listOf<AndroidScreen<*>>(EmptyRendering) + backstack.asList())
Expand All @@ -668,6 +828,26 @@ internal class ComposeViewTreeIntegrationTest {
}
}

data class ViewStubWrapper<C : Screen>(
override val content: C
) : Screen, Wrapper<Screen, C>, AndroidScreen<ViewStubWrapper<C>> {
override fun <D : Screen> map(transform: (C) -> D) = ViewStubWrapper(transform(content))

override val viewFactory: ScreenViewFactory<ViewStubWrapper<C>> =
fromCode { _, initialEnvironment, context, _ ->
val stub = WorkflowViewStub(context)

FrameLayout(context)
.apply {
this.addView(stub)
}.let {
ScreenViewHolder(initialEnvironment, it) { r, e ->
stub.show(r.content, e)
}
}
}
}

/**
* This is our own custom lovingly handcrafted implementation that creates [ComposeView]
* itself, bypassing [ScreenComposableFactory] entirely. Allows us to mess with alternative
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.remember
import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.compose.ui.platform.ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed
import androidx.compose.ui.viewinterop.AndroidView
import androidx.lifecycle.setViewTreeLifecycleOwner
import com.squareup.workflow1.ui.Screen
Expand Down Expand Up @@ -125,6 +126,7 @@ public fun <ScreenT : Screen> ScreenComposableFactory<ScreenT>.asViewFactory():
container: ViewGroup?
): ScreenViewHolder<ScreenT> {
val view = ComposeView(context)
view.setViewCompositionStrategy(DisposeOnViewTreeLifecycleDestroyed)
return ScreenViewHolder(initialEnvironment, view) { newRendering, environment ->

// Update the state whenever a new rendering is emitted.
Expand Down
Loading