Skip to content

Commit

Permalink
fun ComponentDialog.setContent() replaces ScreenOverlayDialogFactory
Browse files Browse the repository at this point in the history
With AndroidX 1.6.0, `ComponentDialog` serves as its own `LifecycleOwner` and `OnBackPressedDispatcherOwner`. To take advantage of this we introduce a new `ComponentDialog.setContent` extension function, deprecate `ScreenOverlayDialogFactory`, and deprecate our hooks for customizing `Dialog` back press handling.

Related kdoc is improved, and a factory function is bound to `OverlayDialogFactory.Companion`.
  • Loading branch information
rjrjr committed Jun 15, 2023
1 parent 4d18454 commit 8d1228d
Show file tree
Hide file tree
Showing 15 changed files with 244 additions and 155 deletions.
Original file line number Diff line number Diff line change
@@ -1,42 +1,36 @@
package com.squareup.sample.container.panel

import android.app.Dialog
import android.content.Context
import android.graphics.Rect
import androidx.appcompat.app.AppCompatDialog
import com.squareup.sample.container.R
import com.squareup.workflow1.ui.Screen
import com.squareup.workflow1.ui.ScreenViewHolder
import com.squareup.workflow1.ui.ViewEnvironment
import com.squareup.workflow1.ui.WorkflowUiExperimentalApi
import com.squareup.workflow1.ui.container.OverlayDialogFactory
import com.squareup.workflow1.ui.container.OverlayDialogHolder
import com.squareup.workflow1.ui.container.ScreenOverlayDialogFactory
import com.squareup.workflow1.ui.container.setBounds
import com.squareup.workflow1.ui.container.setContent
import com.squareup.workflow1.ui.show
import kotlin.reflect.KClass

/**
* Android support for [PanelOverlay].
*/
@OptIn(WorkflowUiExperimentalApi::class)
internal object PanelOverlayDialogFactory :
ScreenOverlayDialogFactory<Screen, PanelOverlay<Screen>>(
type = PanelOverlay::class
) {
/**
* Forks the default implementation to apply [R.style.PanelDialog] for
* enter and exit animation, and to customize [bounds][OverlayDialogHolder.onUpdateBounds].
*/
override fun buildDialogWithContent(
internal object PanelOverlayDialogFactory : OverlayDialogFactory<PanelOverlay<Screen>> {
override val type: KClass<in PanelOverlay<Screen>> = PanelOverlay::class

override fun buildDialog(
initialRendering: PanelOverlay<Screen>,
initialEnvironment: ViewEnvironment,
content: ScreenViewHolder<Screen>
context: Context
): OverlayDialogHolder<PanelOverlay<Screen>> {
val dialog = Dialog(content.view.context, R.style.PanelDialog)
dialog.setContent(content)
val dialog = AppCompatDialog(context, R.style.PanelDialog)

val realHolder = dialog.setContent(initialRendering, initialEnvironment)

return OverlayDialogHolder(
initialEnvironment = initialEnvironment,
dialog = dialog,
onUpdateBounds = { bounds ->
return object : OverlayDialogHolder<PanelOverlay<Screen>> by realHolder {
override val onUpdateBounds: ((Rect) -> Unit) = { bounds ->
val refinedBounds: Rect = if (!dialog.context.isTablet) {
// On a phone, fill the bounds entirely.
bounds
Expand All @@ -62,8 +56,6 @@ internal object PanelOverlayDialogFactory :

dialog.setBounds(refinedBounds)
}
) { overlayRendering, environment ->
content.show(overlayRendering.content, environment)
}
}
}
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 androidx.activity.ComponentDialog
import androidx.compose.foundation.clickable
import androidx.compose.foundation.text.BasicText
import androidx.compose.runtime.Composable
Expand Down Expand Up @@ -36,8 +37,9 @@ import com.squareup.workflow1.ui.WorkflowUiExperimentalApi
import com.squareup.workflow1.ui.container.AndroidOverlay
import com.squareup.workflow1.ui.container.BackStackScreen
import com.squareup.workflow1.ui.container.BodyAndOverlaysScreen
import com.squareup.workflow1.ui.container.OverlayDialogFactory
import com.squareup.workflow1.ui.container.ScreenOverlay
import com.squareup.workflow1.ui.container.ScreenOverlayDialogFactory
import com.squareup.workflow1.ui.container.setContent
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 @@ -374,7 +376,7 @@ internal class ComposeViewTreeIntegrationTest {
it.setRendering(
BodyAndOverlaysScreen(
EmptyRendering,
TestModal(BackStackScreen(EmptyRendering, firstScreen))
TestOverlay(BackStackScreen(EmptyRendering, firstScreen))
)
)
}
Expand Down Expand Up @@ -426,9 +428,9 @@ internal class ComposeViewTreeIntegrationTest {
it.setRendering(
BodyAndOverlaysScreen(
EmptyRendering,
TestModal(firstScreen),
TestModal(secondScreen),
TestModal(thirdScreen)
TestOverlay(firstScreen),
TestOverlay(secondScreen),
TestOverlay(thirdScreen)
)
)
}
Expand Down Expand Up @@ -488,11 +490,11 @@ internal class ComposeViewTreeIntegrationTest {
it.setRendering(
BodyAndOverlaysScreen(
EmptyRendering,
TestModal(BackStackScreen(EmptyRendering, layer0Screen0)),
TestOverlay(BackStackScreen(EmptyRendering, layer0Screen0)),
// A SavedStateRegistry is set up for each modal. Each registry needs a unique name,
// and these names default to their `Compatible.keyFor` value. When we show two
// of the same type at the same time, we need to give them unique names.
TestModal(NamedScreen(BackStackScreen(EmptyRendering, layer1Screen0), "another")),
TestOverlay(NamedScreen(BackStackScreen(EmptyRendering, layer1Screen0), "another")),
)
)
}
Expand All @@ -514,11 +516,11 @@ internal class ComposeViewTreeIntegrationTest {
it.setRendering(
BodyAndOverlaysScreen(
EmptyRendering,
TestModal(BackStackScreen(EmptyRendering, layer0Screen0, layer0Screen1)),
TestOverlay(BackStackScreen(EmptyRendering, layer0Screen0, layer0Screen1)),
// A SavedStateRegistry is set up for each modal. Each registry needs a unique name,
// and these names default to their `Compatible.keyFor` value. When we show two
// of the same type at the same time, we need to give them unique names.
TestModal(
TestOverlay(
NamedScreen(BackStackScreen(EmptyRendering, layer1Screen0, layer1Screen1), "another")
),
)
Expand Down Expand Up @@ -556,11 +558,11 @@ internal class ComposeViewTreeIntegrationTest {
it.setRendering(
BodyAndOverlaysScreen(
EmptyRendering,
TestModal(BackStackScreen(EmptyRendering, layer0Screen0)),
TestOverlay(BackStackScreen(EmptyRendering, layer0Screen0)),
// A SavedStateRegistry is set up for each modal. Each registry needs a unique name,
// and these names default to their `Compatible.keyFor` value. When we show two
// of the same type at the same time, we need to give them unique names.
TestModal(NamedScreen(BackStackScreen(EmptyRendering, layer1Screen0), "another")),
TestOverlay(NamedScreen(BackStackScreen(EmptyRendering, layer1Screen0), "another")),
)
)
}
Expand All @@ -575,15 +577,15 @@ internal class ComposeViewTreeIntegrationTest {
setRendering(BackStackScreen(EmptyRendering, backstack.asList()))
}

data class TestModal(
data class TestOverlay(
override val content: Screen
) : ScreenOverlay<Screen>, AndroidOverlay<TestModal> {
override fun <ContentU : Screen> map(transform: (Screen) -> ContentU) = error("Not implemented")
) : ScreenOverlay<Screen>, AndroidOverlay<TestOverlay> {
override fun <U : Screen> map(transform: (Screen) -> U) = error("Not implemented")

override val dialogFactory = object : ScreenOverlayDialogFactory<Screen, TestModal>(
TestModal::class
) {
}
override val dialogFactory =
OverlayDialogFactory<TestOverlay> { initialRendering, initialEnvironment, context: Context ->
ComponentDialog(context).setContent(initialRendering, initialEnvironment)
}
}

data class TestComposeRendering(
Expand Down
8 changes: 8 additions & 0 deletions workflow-ui/core-android/api/core-android.api
Original file line number Diff line number Diff line change
Expand Up @@ -363,6 +363,10 @@ public final class com/squareup/workflow1/ui/container/BackStackContainer$SavedS
public synthetic fun newArray (I)[Ljava/lang/Object;
}

public final class com/squareup/workflow1/ui/container/ComponentDialogSetContentKt {
public static final fun setContent (Landroidx/activity/ComponentDialog;Lcom/squareup/workflow1/ui/container/ScreenOverlay;Lcom/squareup/workflow1/ui/ViewEnvironment;)Lcom/squareup/workflow1/ui/container/OverlayDialogHolder;
}

public final class com/squareup/workflow1/ui/container/LayeredDialogSessions {
public static final field Companion Lcom/squareup/workflow1/ui/container/LayeredDialogSessions$Companion;
public synthetic fun <init> (Landroid/content/Context;Lkotlinx/coroutines/flow/StateFlow;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/internal/DefaultConstructorMarker;)V
Expand Down Expand Up @@ -393,9 +397,13 @@ public final class com/squareup/workflow1/ui/container/LayeredDialogSessions$Sav
}

public abstract interface class com/squareup/workflow1/ui/container/OverlayDialogFactory : com/squareup/workflow1/ui/ViewRegistry$Entry {
public static final field Companion Lcom/squareup/workflow1/ui/container/OverlayDialogFactory$Companion;
public abstract fun buildDialog (Lcom/squareup/workflow1/ui/container/Overlay;Lcom/squareup/workflow1/ui/ViewEnvironment;Landroid/content/Context;)Lcom/squareup/workflow1/ui/container/OverlayDialogHolder;
}

public final class com/squareup/workflow1/ui/container/OverlayDialogFactory$Companion {
}

public abstract interface class com/squareup/workflow1/ui/container/OverlayDialogFactoryFinder {
public static final field Companion Lcom/squareup/workflow1/ui/container/OverlayDialogFactoryFinder$Companion;
public abstract fun getDialogFactoryForRendering (Lcom/squareup/workflow1/ui/ViewEnvironment;Lcom/squareup/workflow1/ui/container/Overlay;)Lcom/squareup/workflow1/ui/container/OverlayDialogFactory;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@ package com.squareup.workflow1.ui.container

import android.app.Dialog
import android.text.SpannableStringBuilder
import android.view.View
import android.view.ViewGroup.LayoutParams
import android.view.ViewGroup.LayoutParams.MATCH_PARENT
import android.widget.EditText
import androidx.activity.ComponentActivity
import androidx.activity.ComponentDialog
import androidx.lifecycle.Lifecycle.State.DESTROYED
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.assertion.ViewAssertions.matches
Expand Down Expand Up @@ -53,7 +53,6 @@ internal class DialogIntegrationTest {
}
}

private var latestContentView: View? = null
private var latestDialog: Dialog? = null

private inner class DialogRendering(
Expand All @@ -65,22 +64,10 @@ internal class DialogIntegrationTest {

override val compatibilityKey = name

override val dialogFactory =
object : ScreenOverlayDialogFactory<ContentRendering, DialogRendering>(
type = DialogRendering::class
) {
override fun buildDialogWithContent(
initialRendering: DialogRendering,
initialEnvironment: ViewEnvironment,
content: ScreenViewHolder<ContentRendering>
): OverlayDialogHolder<DialogRendering> {
latestContentView = content.view

return super.buildDialogWithContent(initialRendering, initialEnvironment, content).also {
latestDialog = it.dialog
}
}
}
override val dialogFactory = OverlayDialogFactory<DialogRendering> { r, e, c ->
val dialog = ComponentDialog(c).also { latestDialog = it }
dialog.setContent(r, e)
}
}

@Test fun showOne() {
Expand All @@ -96,7 +83,6 @@ internal class DialogIntegrationTest {
}
onView(withText("content")).inRoot(isDialog()).check(matches(isDisplayed()))

assertThat(latestContentView).isNotNull()
assertThat(latestDialog).isNotNull()
assertThat(latestDialog!!.isShowing).isTrue()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import kotlin.reflect.KClass

/**
* Default [OverlayDialogFactory] for [AlertOverlay], uses [AlertDialog].
*
* See [AlertDialog.toDialogHolder] to use [AlertDialog] for other purposes.
*
* - To customize [AlertDialog] theming, see [AlertDialogThemeResId]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,19 @@ import com.squareup.workflow1.ui.WorkflowUiExperimentalApi
*
* - [ScreenOverlay] for dialogs whose content is defined by a wrapped
* [Screen][com.squareup.workflow1.ui.Screen] instance. And in this case,
* also note [ScreenOverlayDialogFactory].
* also note the [ComponentDialog.setContent][setContent] extension function.
*
* For example:
*
* data class MyModal<C : Screen>(
* override val content: C
* ) : ScreenOverlay<C>, ModalOverlay, AndroidOverlay<MyModal<C>> {
* override val dialogFactory = OverlayDialogFactory<MyModal<C>> { r, e, c ->
* AppCompatDialog(c).setContent(r, e)
* }
*
* override fun <D : Screen> map(transform: (C) -> D) = MyModal(transform(content))
* }
*
* This is the simplest way to introduce a [Dialog][android.app.Dialog] workflow driven UI,
* but using it requires your workflows code to reside in Android modules, instead
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,6 @@ import com.squareup.workflow1.ui.screen
* work as possible is delegated to the public [LayeredDialogSessions]
* support class, to make it practical to write custom forks should
* the need arise.
*
* See [ScreenOverlayDialogFactory] for a general overview of how
* Workflow's [android.app.Dialog] support actually works.
*/
@WorkflowUiExperimentalApi
internal class BodyAndOverlaysContainer @JvmOverloads constructor(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package com.squareup.workflow1.ui.container

import android.graphics.drawable.ColorDrawable
import android.util.TypedValue
import android.view.WindowManager.LayoutParams.FLAG_DIM_BEHIND
import androidx.activity.ComponentDialog
import androidx.activity.setViewTreeOnBackPressedDispatcherOwner
import com.squareup.workflow1.ui.Screen
import com.squareup.workflow1.ui.ViewEnvironment
import com.squareup.workflow1.ui.WorkflowUiExperimentalApi
import com.squareup.workflow1.ui.androidx.WorkflowLifecycleOwner
import com.squareup.workflow1.ui.show
import com.squareup.workflow1.ui.startShowing
import com.squareup.workflow1.ui.toViewFactory

/**
* Given a [ComponentDialog], wrap it in an [OverlayDialogHolder] that can drive
* the Dialog's content via instances of a particular type of [ScreenOverlay].
*
* Dialogs managed this way are compatible with
* [View.setBackHandler][com.squareup.workflow1.ui.setBackHandler],
* and honor the [OverlayArea] and [CoveredByModal] values placed in
* the [ViewEnvironment] by the standard [BodyAndOverlaysScreen] container.
*/
@WorkflowUiExperimentalApi
public fun <C : Screen, O : ScreenOverlay<C>> ComponentDialog.setContent(
overlay: O,
environment: ViewEnvironment
): OverlayDialogHolder<O> {
val contentHolder = overlay.content.toViewFactory(environment)
.startShowing(overlay.content, environment, context) { view, doStart ->
view.setViewTreeOnBackPressedDispatcherOwner(this@setContent)
WorkflowLifecycleOwner.installOn(view) {
this@setContent.lifecycle
}
doStart()
}

setCancelable(false)
setContentView(contentHolder.view)

// Welcome to Android. Nothing workflow-related here, this is just how one
// finds the window background color for the theme. I sure hope it's better in Compose.
val maybeWindowColor = TypedValue()
context.theme.resolveAttribute(android.R.attr.windowBackground, maybeWindowColor, true)

val background =
if (maybeWindowColor.type in TypedValue.TYPE_FIRST_COLOR_INT..TypedValue.TYPE_LAST_COLOR_INT) {
ColorDrawable(maybeWindowColor.data)
} else {
// If we don't at least set it to null, the window cannot go full bleed.
null
}
with(window!!) {
setBackgroundDrawable(background)
clearFlags(FLAG_DIM_BEHIND)
}

return OverlayDialogHolder(
initialEnvironment = environment,
dialog = this,
) { newOverlay, newEnvironment ->
contentHolder.show(newOverlay.content, newEnvironment)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ internal class DialogSession(
/**
* Wrap the given dialog holder to maintain [allowEvents] on each update.
*/
@Suppress("DEPRECATION")
private val holder: OverlayDialogHolder<Overlay> = OverlayDialogHolder(
holder.environment,
holder.dialog,
Expand Down Expand Up @@ -109,6 +110,7 @@ internal class DialogSession(
return !allowEvents || realWindowCallback.dispatchTouchEvent(event)
}

@Suppress("DEPRECATION")
override fun dispatchKeyEvent(event: KeyEvent): Boolean {
// Consume all events if we've been told to do so.
if (!allowEvents) return true
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package com.squareup.workflow1.ui.container

import android.content.Context
import androidx.activity.ComponentDialog
import com.squareup.workflow1.ui.Screen
import com.squareup.workflow1.ui.ViewEnvironment
import com.squareup.workflow1.ui.WorkflowUiExperimentalApi

/**
* Default [OverlayDialogFactory] for the standard [FullScreenModal] rendering class.
* Nothing more than a direct call to [ComponentDialog.setContent].
*
* To provide a custom binding for [FullScreenModal], see [OverlayDialogFactoryFinder].
*/
@WorkflowUiExperimentalApi
internal class FullScreenModalFactory<C : Screen>() : OverlayDialogFactory<FullScreenModal<C>> {
override val type = FullScreenModal::class

override fun buildDialog(
initialRendering: FullScreenModal<C>,
initialEnvironment: ViewEnvironment,
context: Context
): OverlayDialogHolder<FullScreenModal<C>> =
ComponentDialog(context).setContent(initialRendering, initialEnvironment)
}
Loading

0 comments on commit 8d1228d

Please sign in to comment.