From 9c91a20ee86ace76b30d41939bb6b6178fbb15fd Mon Sep 17 00:00:00 2001 From: Ray Ryan Date: Tue, 11 Jan 2022 09:44:18 -0800 Subject: [PATCH] Introduces ScreenViewFactoryFinder Fixes #594, which was about how impractical it is to wrap the `ViewRegistry` interface, by introducing a higher level interface that's easy to wrap. The problem is that the fundamental method is `getFactoryFor(KClass)`, but lots of crucial behavior (e.g. `AndroidViewRendering` / `AndroidScreenRendering`) is in the `getFactoryForRendering` extension method. But if we change the fundamental method to be instance based instead of type based, we bring back a lot of potential complexity to `ViewRegistry` that the original type-based choice very intentionally restricted. To have our cake and eat it too, we move the extension method to a new `ViewEnvironment` service interface, `ScreenViewFactoryFinder`. Ta da, totally customizable, with the defaults totally built in. --- workflow-ui/core-android/api/core-android.api | 14 +++ .../workflow1/ui/AndroidViewRegistry.kt | 2 +- .../workflow1/ui/ScreenViewFactory.kt | 35 +------- .../workflow1/ui/ScreenViewFactoryFinder.kt | 85 +++++++++++++++++++ .../workflow1/ui/ScreenViewFactoryTest.kt | 2 +- 5 files changed, 104 insertions(+), 34 deletions(-) create mode 100644 workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/ScreenViewFactoryFinder.kt diff --git a/workflow-ui/core-android/api/core-android.api b/workflow-ui/core-android/api/core-android.api index 456cd95b6d..c8f2e368d6 100644 --- a/workflow-ui/core-android/api/core-android.api +++ b/workflow-ui/core-android/api/core-android.api @@ -95,6 +95,20 @@ public final class com/squareup/workflow1/ui/ScreenViewFactory$DefaultImpls { public static synthetic fun buildView$default (Lcom/squareup/workflow1/ui/ScreenViewFactory;Lcom/squareup/workflow1/ui/Screen;Lcom/squareup/workflow1/ui/ViewEnvironment;Landroid/content/Context;Landroid/view/ViewGroup;ILjava/lang/Object;)Landroid/view/View; } +public abstract interface class com/squareup/workflow1/ui/ScreenViewFactoryFinder { + public static final field Companion Lcom/squareup/workflow1/ui/ScreenViewFactoryFinder$Companion; + public abstract fun getViewFactoryForRendering (Lcom/squareup/workflow1/ui/ViewEnvironment;Lcom/squareup/workflow1/ui/Screen;)Lcom/squareup/workflow1/ui/ScreenViewFactory; +} + +public final class com/squareup/workflow1/ui/ScreenViewFactoryFinder$Companion : com/squareup/workflow1/ui/ViewEnvironmentKey { + public fun getDefault ()Lcom/squareup/workflow1/ui/ScreenViewFactoryFinder; + public synthetic fun getDefault ()Ljava/lang/Object; +} + +public final class com/squareup/workflow1/ui/ScreenViewFactoryFinder$DefaultImpls { + public static fun getViewFactoryForRendering (Lcom/squareup/workflow1/ui/ScreenViewFactoryFinder;Lcom/squareup/workflow1/ui/ViewEnvironment;Lcom/squareup/workflow1/ui/Screen;)Lcom/squareup/workflow1/ui/ScreenViewFactory; +} + public final class com/squareup/workflow1/ui/ScreenViewFactoryKt { public static final fun buildView (Lcom/squareup/workflow1/ui/Screen;Lcom/squareup/workflow1/ui/ViewEnvironment;Landroid/content/Context;Landroid/view/ViewGroup;Lcom/squareup/workflow1/ui/ViewStarter;)Landroid/view/View; public static synthetic fun buildView$default (Lcom/squareup/workflow1/ui/Screen;Lcom/squareup/workflow1/ui/ViewEnvironment;Landroid/content/Context;Landroid/view/ViewGroup;Lcom/squareup/workflow1/ui/ViewStarter;ILjava/lang/Object;)Landroid/view/View; diff --git a/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/AndroidViewRegistry.kt b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/AndroidViewRegistry.kt index 52b765db0a..8c53d418c9 100644 --- a/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/AndroidViewRegistry.kt +++ b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/AndroidViewRegistry.kt @@ -8,7 +8,7 @@ import android.view.ViewGroup import com.squareup.workflow1.ui.container.BackStackScreen import kotlin.reflect.KClass -@Deprecated("Use ViewEnvironment.getViewFactoryForRendering()") +@Deprecated("Use ScreenViewFactoryFinder.getViewFactoryForRendering()") @WorkflowUiExperimentalApi public fun ViewRegistry.getFactoryForRendering(rendering: RenderingT): ViewFactory { diff --git a/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/ScreenViewFactory.kt b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/ScreenViewFactory.kt index a674dfbbf6..17d570b76d 100644 --- a/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/ScreenViewFactory.kt +++ b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/ScreenViewFactory.kt @@ -3,12 +3,6 @@ package com.squareup.workflow1.ui import android.content.Context import android.view.View import android.view.ViewGroup -import com.squareup.workflow1.ui.container.BackStackScreen -import com.squareup.workflow1.ui.container.BackStackScreenViewFactory -import com.squareup.workflow1.ui.container.BodyAndModalsContainer -import com.squareup.workflow1.ui.container.BodyAndModalsScreen -import com.squareup.workflow1.ui.container.EnvironmentScreen -import com.squareup.workflow1.ui.container.EnvironmentScreenViewFactory /** * Factory for [View] instances that can show renderings of type [RenderingT] : [Screen]. @@ -63,7 +57,9 @@ public fun ScreenT.buildView( container: ViewGroup? = null, viewStarter: ViewStarter? = null, ): View { - val viewFactory = viewEnvironment.getViewFactoryForRendering(this) + val viewFactory = viewEnvironment[ScreenViewFactoryFinder].getViewFactoryForRendering( + viewEnvironment, this + ) return viewFactory.buildView(this, viewEnvironment, contextForNewView, container).also { view -> checkNotNull(view.workflowViewStateOrNull) { @@ -97,28 +93,3 @@ public fun interface ViewStarter { doStart: () -> Unit ) } - -@WorkflowUiExperimentalApi -internal fun - ViewEnvironment.getViewFactoryForRendering(rendering: ScreenT): ScreenViewFactory { - val entry = get(ViewRegistry).getEntryFor(rendering::class) - - @Suppress("UNCHECKED_CAST", "DEPRECATION") - return (entry as? ScreenViewFactory) - ?: (rendering as? AndroidScreen<*>)?.viewFactory as? ScreenViewFactory - ?: (rendering as? AsScreen<*>)?.let { AsScreenViewFactory as ScreenViewFactory } - ?: (rendering as? BackStackScreen<*>)?.let { - BackStackScreenViewFactory as ScreenViewFactory - } - ?: (rendering as? BodyAndModalsScreen<*, *>)?.let { - BodyAndModalsContainer as ScreenViewFactory - } - ?: (rendering as? NamedScreen<*>)?.let { NamedScreenViewFactory as ScreenViewFactory } - ?: (rendering as? EnvironmentScreen<*>)?.let { - EnvironmentScreenViewFactory as ScreenViewFactory - } - ?: throw IllegalArgumentException( - "A ScreenViewFactory should have been registered to display $rendering, " + - "or that class should implement AndroidScreen. Instead found $entry." - ) -} diff --git a/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/ScreenViewFactoryFinder.kt b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/ScreenViewFactoryFinder.kt new file mode 100644 index 0000000000..41d53a28f3 --- /dev/null +++ b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/ScreenViewFactoryFinder.kt @@ -0,0 +1,85 @@ +package com.squareup.workflow1.ui + +import com.squareup.workflow1.ui.container.BackStackScreen +import com.squareup.workflow1.ui.container.BackStackScreenViewFactory +import com.squareup.workflow1.ui.container.BodyAndModalsContainer +import com.squareup.workflow1.ui.container.BodyAndModalsScreen +import com.squareup.workflow1.ui.container.EnvironmentScreen +import com.squareup.workflow1.ui.container.EnvironmentScreenViewFactory + +/** + * [ViewEnvironment] service object used by [Screen.buildView] to find the right + * [ScreenViewFactory]. The default implementation makes [AndroidScreen] work + * and provides default bindings for [NamedScreen], [EnvironmentScreen], [BackStackScreen], + * etc. + * + * Here is how this hook could be used to provide a custom view to handle [BackStackScreen]: + * + * object MyViewFactory : ScreenViewFactory> + * by ManualScreenViewFactory( + * type = BackStackScreen::class, + * viewConstructor = { initialRendering, initialEnv, context, _ -> + * MyBackStackContainer(context) + * .apply { + * layoutParams = (LayoutParams(MATCH_PARENT, MATCH_PARENT)) + * bindShowRendering(initialRendering, initialEnv, ::update) + * } + * } + * ) + * + * object MyFinder : ScreenViewFactoryFinder { + * @Suppress("UNCHECKED_CAST") + * if (rendering is BackStackScreen<*>) + * return MyViewFactory as ScreenViewFactory + * return super.getViewFactoryForRendering(environment, rendering) + * } + * + * class MyViewModel(savedState: SavedStateHandle) : ViewModel() { + * val renderings: StateFlow by lazy { + * val customized = ViewEnvironment() + (ScreenViewFactoryFinder to MyFinder) + * renderWorkflowIn( + * workflow = MyRootWorkflow.withEnvironment(customized), + * scope = viewModelScope, + * savedStateHandle = savedState + * ) + * } + * } + */ + +@WorkflowUiExperimentalApi +public interface ScreenViewFactoryFinder { + public fun getViewFactoryForRendering( + environment: ViewEnvironment, + rendering: ScreenT + ): ScreenViewFactory { + val entry = environment[ViewRegistry].getEntryFor(rendering::class) + + @Suppress("UNCHECKED_CAST", "DEPRECATION") + return (entry as? ScreenViewFactory) + ?: (rendering as? AndroidScreen<*>)?.viewFactory as? ScreenViewFactory + ?: (rendering as? AsScreen<*>)?.let { AsScreenViewFactory as ScreenViewFactory } + ?: (rendering as? BackStackScreen<*>)?.let { + BackStackScreenViewFactory as ScreenViewFactory + } + ?: (rendering as? BodyAndModalsScreen<*, *>)?.let { + BodyAndModalsContainer as ScreenViewFactory + } + ?: (rendering as? NamedScreen<*>)?.let { + NamedScreenViewFactory as ScreenViewFactory + } + ?: (rendering as? EnvironmentScreen<*>)?.let { + EnvironmentScreenViewFactory as ScreenViewFactory + } + ?: throw IllegalArgumentException( + "A ScreenViewFactory should have been registered to display $rendering, " + + "or that class should implement AndroidScreen. Instead found $entry." + ) + } + + public companion object : ViewEnvironmentKey( + ScreenViewFactoryFinder::class + ) { + override val default: ScreenViewFactoryFinder + get() = object : ScreenViewFactoryFinder {} + } +} diff --git a/workflow-ui/core-android/src/test/java/com/squareup/workflow1/ui/ScreenViewFactoryTest.kt b/workflow-ui/core-android/src/test/java/com/squareup/workflow1/ui/ScreenViewFactoryTest.kt index d650b901f9..0cbb9c622d 100644 --- a/workflow-ui/core-android/src/test/java/com/squareup/workflow1/ui/ScreenViewFactoryTest.kt +++ b/workflow-ui/core-android/src/test/java/com/squareup/workflow1/ui/ScreenViewFactoryTest.kt @@ -74,7 +74,7 @@ internal class ScreenViewFactoryTest { return mock { on { - getTag(eq(com.squareup.workflow1.ui.R.id.workflow_ui_view_state)) + getTag(eq(R.id.workflow_ui_view_state)) } doReturn (WorkflowViewState.New(initialRendering, initialViewEnvironment, { _, _ -> })) } }