From 2b532209b758dcbb6d7728ab49215034d606d988 Mon Sep 17 00:00:00 2001 From: Ray Ryan Date: Thu, 1 Oct 2020 09:12:32 -0700 Subject: [PATCH 1/2] ViewRegistry, LayoutRunner tidying Deletes unused method `ViewRegistry.hasViewBeenBound`. Reduces visibility of `LayoutRunnerViewFactory`. Improves kdoc on `LayoutRunner`, especially WRT `ViewBinding`. Also deletes `ViewRegistryTest`, which was mainly about testing an impossible to mock extension method that is thoroughly exercised by espresso tests, and was the only reason `ViewRegistry.hasViewBeenBound` existed. closes #197 --- .../overviewdetail/OverviewDetailContainer.kt | 6 +- workflow-ui/core-android/api/core-android.api | 5 - .../com/squareup/workflow1/ui/LayoutRunner.kt | 129 ++++++------------ .../workflow1/ui/LayoutRunnerViewFactory.kt | 3 +- .../com/squareup/workflow1/ui/ViewRegistry.kt | 23 +--- .../squareup/workflow1/ui/ViewRegistryTest.kt | 74 ---------- 6 files changed, 52 insertions(+), 188 deletions(-) delete mode 100644 workflow-ui/core-android/src/test/java/com/squareup/workflow1/ui/ViewRegistryTest.kt diff --git a/samples/containers/android/src/main/java/com/squareup/sample/container/overviewdetail/OverviewDetailContainer.kt b/samples/containers/android/src/main/java/com/squareup/sample/container/overviewdetail/OverviewDetailContainer.kt index 194ac4dd99..3453d43b82 100644 --- a/samples/containers/android/src/main/java/com/squareup/sample/container/overviewdetail/OverviewDetailContainer.kt +++ b/samples/containers/android/src/main/java/com/squareup/sample/container/overviewdetail/OverviewDetailContainer.kt @@ -24,7 +24,6 @@ import com.squareup.sample.container.overviewdetail.OverviewDetailConfig.Overvie import com.squareup.sample.container.overviewdetail.OverviewDetailConfig.Single import com.squareup.workflow1.ui.WorkflowUiExperimentalApi import com.squareup.workflow1.ui.LayoutRunner -import com.squareup.workflow1.ui.LayoutRunnerViewFactory import com.squareup.workflow1.ui.ViewFactory import com.squareup.workflow1.ui.ViewEnvironment import com.squareup.workflow1.ui.WorkflowViewStub @@ -98,9 +97,8 @@ class OverviewDetailContainer(view: View) : LayoutRunner { stub.update(combined, viewEnvironment + (OverviewDetailConfig to Single)) } - companion object : ViewFactory by LayoutRunnerViewFactory( - type = OverviewDetailScreen::class, + companion object : ViewFactory by LayoutRunner.bind( layoutId = R.layout.overview_detail, - runnerConstructor = ::OverviewDetailContainer + constructor = ::OverviewDetailContainer ) } diff --git a/workflow-ui/core-android/api/core-android.api b/workflow-ui/core-android/api/core-android.api index db25932ee6..0509fa7a20 100644 --- a/workflow-ui/core-android/api/core-android.api +++ b/workflow-ui/core-android/api/core-android.api @@ -105,7 +105,6 @@ public abstract interface class com/squareup/workflow1/ui/ViewRegistry { public static final field Companion Lcom/squareup/workflow1/ui/ViewRegistry$Companion; public abstract fun getFactoryFor (Lkotlin/reflect/KClass;)Lcom/squareup/workflow1/ui/ViewFactory; public abstract fun getKeys ()Ljava/util/Set; - public abstract fun hasViewBeenBound (Landroid/view/View;)Z } public final class com/squareup/workflow1/ui/ViewRegistry$Companion : com/squareup/workflow1/ui/ViewEnvironmentKey { @@ -113,10 +112,6 @@ public final class com/squareup/workflow1/ui/ViewRegistry$Companion : com/square public synthetic fun getDefault ()Ljava/lang/Object; } -public final class com/squareup/workflow1/ui/ViewRegistry$DefaultImpls { - public static fun hasViewBeenBound (Lcom/squareup/workflow1/ui/ViewRegistry;Landroid/view/View;)Z -} - public final class com/squareup/workflow1/ui/ViewRegistryKt { public static final fun ViewRegistry ()Lcom/squareup/workflow1/ui/ViewRegistry; public static final fun ViewRegistry ([Lcom/squareup/workflow1/ui/ViewFactory;)Lcom/squareup/workflow1/ui/ViewRegistry; diff --git a/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/LayoutRunner.kt b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/LayoutRunner.kt index 8cf40b6187..2f6e0e0b2c 100644 --- a/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/LayoutRunner.kt +++ b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/LayoutRunner.kt @@ -1,18 +1,3 @@ -/* - * Copyright 2019 Square Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ package com.squareup.workflow1.ui import android.content.Context @@ -32,54 +17,8 @@ typealias ViewBindingInflater = (LayoutInflater, ViewGroup?, Boolean) * by a [ViewRegistry]. (Use [BuilderViewFactory] if you want to build views from code rather * than layouts.) * - * Typical usage is to have a [LayoutRunner]'s `companion object` implement - * [ViewFactory] by delegating to [LayoutRunner.bind], specifying the layout resource - * it expects to drive. - * - * class HelloLayoutRunner(view: View) : LayoutRunner { - * private val messageView: TextView = view.findViewById(R.id.hello_message) - * - * override fun showRendering(rendering: Rendering) { - * messageView.text = rendering.message - * messageView.setOnClickListener { rendering.onClick(Unit) } - * } - * - * companion object : ViewFactory by bind( - * R.layout.hello_goodbye_layout, ::HelloLayoutRunner - * ) - * } - * - * This pattern allows us to assemble a [ViewRegistry] out of the [LayoutRunner] classes - * themselves. - * - * val TicTacToeViewBuilders = ViewRegistry( - * NewGameLayoutRunner, GamePlayLayoutRunner, GameOverLayoutRunner - * ) - * - * ## AndroidX ViewBinding - * - * [AndroidX ViewBinding][ViewBinding] is supported in two ways. - * In most cases, you can use the `bind` function that takes a function and avoid implementing - * [LayoutRunner] at all. - * - * If you need to perform some set up before [showRendering] is called, use the - * `bind` overload that takes: - * - a reference to a `ViewBinding.inflate` method and - * - a [LayoutRunner] constructor that accepts a [ViewBinding] - * - * class HelloLayoutRunner(private val binding: HelloGoodbyeLayoutBinding) : LayoutRunner { - * - * override fun showRendering(rendering: Rendering) { - * binding.messageView.text = rendering.message - * binding.messageView.setOnClickListener { rendering.onClick(Unit) } - * } - * - * companion object : ViewFactory by bind( - * HelloGoodbyeLayoutBinding::inflate, ::HelloLayoutRunner - * ) - * } - * - * If the view does not need to be initialized, the [bind] function can be used instead. + * If you're using [AndroidX ViewBinding][ViewBinding] you likely won't need to + * implement this interface at all. For details, see the three overloads of [LayoutRunner.bind]. */ @WorkflowUiExperimentalApi interface LayoutRunner { @@ -90,28 +29,18 @@ interface LayoutRunner { companion object { /** - * Creates a [ViewFactory] that inflates [layoutId] to show renderings of type [RenderingT], - * using a [LayoutRunner] created by [constructor]. - */ - inline fun bind( - @LayoutRes layoutId: Int, - noinline constructor: (View) -> LayoutRunner - ): ViewFactory = LayoutRunnerViewFactory(RenderingT::class, layoutId, constructor) - - /** - * Creates a [ViewFactory] that [inflates][bindingInflater] a [ViewBinding] ([BindingT]) to show - * renderings of type [RenderingT], using [showRendering]. + * Creates a [ViewFactory] that [inflates][bindingInflater] a [ViewBinding] ([BindingT]) + * to show renderings of type [RenderingT], using [a lambda][showRendering]. * - * ``` - * val HelloBinding: ViewFactory = - * bindViewBinding(HelloGoodbyeLayoutBinding::inflate) { rendering, viewEnvironment -> - * helloMessage.text = rendering.message - * helloMessage.setOnClickListener { rendering.onClick(Unit) } - * } - * ``` + * val HelloBinding: ViewFactory = + * LayoutRunner.bind(HelloGoodbyeLayoutBinding::inflate) { rendering, viewEnvironment -> + * helloMessage.text = rendering.message + * helloMessage.setOnClickListener { rendering.onClick(Unit) } + * } * - * If you need to initialize your view before [showRendering] is called, create a [LayoutRunner] - * and create a binding using `LayoutRunner.bind` instead. + * If you need to initialize your view before [showRendering] is called, + * implement [LayoutRunner] and create a binding using the `bind` variant + * that accepts a `(ViewBinding) -> LayoutRunner` function, below. */ inline fun bind( noinline bindingInflater: ViewBindingInflater, @@ -126,12 +55,26 @@ interface LayoutRunner { } /** - * Creates a [ViewFactory] that [inflates][bindingInflater] a [BindingT] to show renderings of - * type [RenderingT], using a [LayoutRunner] created by [constructor]. + * Creates a [ViewFactory] that [inflates][bindingInflater] a [ViewBinding] ([BindingT]) + * to show renderings of type [RenderingT], using a [LayoutRunner] created by [constructor]. + * Handy if you need to perform some set up before [showRendering] is called. + * + * class HelloLayoutRunner( + * private val binding: HelloGoodbyeLayoutBinding + * ) : LayoutRunner { + * + * override fun showRendering(rendering: Rendering) { + * binding.messageView.text = rendering.message + * binding.messageView.setOnClickListener { rendering.onClick(Unit) } + * } + * + * companion object : ViewFactory by bind( + * HelloGoodbyeLayoutBinding::inflate, ::HelloLayoutRunner + * ) + * } * * If the view doesn't need to be initialized before [showRendering] is called, - * [bind] can be used instead, which just takes a lambda instead requiring a whole - * [LayoutRunner] class. + * use the variant above which just takes a lambda. */ inline fun bind( noinline bindingInflater: ViewBindingInflater, @@ -139,9 +82,19 @@ interface LayoutRunner { ): ViewFactory = ViewBindingViewFactory(RenderingT::class, bindingInflater, constructor) + /** + * Creates a [ViewFactory] that inflates [layoutId] to show renderings of type [RenderingT], + * using a [LayoutRunner] created by [constructor]. Avoids any use of + * [AndroidX ViewBinding][ViewBinding]. + */ + inline fun bind( + @LayoutRes layoutId: Int, + noinline constructor: (View) -> LayoutRunner + ): ViewFactory = LayoutRunnerViewFactory(RenderingT::class, layoutId, constructor) + /** * Creates a [ViewFactory] that inflates [layoutId] to "show" renderings of type [RenderingT], - * with a no-op [LayoutRunner]. Handy for showing static views. + * with a no-op [LayoutRunner]. Handy for showing static views, e.g. when prototyping. */ inline fun bindNoRunner( @LayoutRes layoutId: Int diff --git a/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/LayoutRunnerViewFactory.kt b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/LayoutRunnerViewFactory.kt index c5b14fd5ee..7e5c3810ed 100644 --- a/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/LayoutRunnerViewFactory.kt +++ b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/LayoutRunnerViewFactory.kt @@ -12,7 +12,8 @@ import kotlin.reflect.KClass * details. */ @WorkflowUiExperimentalApi -class LayoutRunnerViewFactory( +@PublishedApi +internal class LayoutRunnerViewFactory( override val type: KClass, @LayoutRes private val layoutId: Int, private val runnerConstructor: (View) -> LayoutRunner diff --git a/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/ViewRegistry.kt b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/ViewRegistry.kt index 44cbff7c43..27a4817afa 100644 --- a/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/ViewRegistry.kt +++ b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/ViewRegistry.kt @@ -34,10 +34,10 @@ internal val defaultViewFactories = ViewRegistry(NamedViewFactory) * * Two concrete [ViewFactory] implementations are provided: * - * - [LayoutRunner.Binding], allowing the easy pairing of Android XML layout resources with - * [LayoutRunner]s to drive them. + * - The various [bind][LayoutRunner.bind] methods on [LayoutRunner] allow easy use of + * Android XML layout resources and [AndroidX ViewBinding][androidx.viewbinding.ViewBinding]. * - * - [BuilderViewFactory], which can build views from code. + * - [BuilderViewFactory] allows views to be built from code. * * Registries can be assembled via concatenation, making it easy to snap together screen sets. * For example: @@ -54,8 +54,7 @@ internal val defaultViewFactories = ViewRegistry(NamedViewFactory) * AuthViewFactories + TicTacToeViewFactories * * In the above example, note that the `companion object`s of the various [LayoutRunner] classes - * honor a convention of implementing [ViewFactory], in aid of this kind of assembly. See the - * class doc on [LayoutRunner] for details. + * honor a convention of implementing [ViewFactory], in aid of this kind of assembly. */ @WorkflowUiExperimentalApi interface ViewRegistry { @@ -79,14 +78,6 @@ interface ViewRegistry { renderingType: KClass ): ViewFactory - /** - * This method is not for general use, it's called by [buildView] to validate views returned by - * [ViewFactory]s. - * - * Returns true iff [view] has been bound to a [ShowRenderingTag] by calling [bindShowRendering]. - */ - fun hasViewBeenBound(view: View): Boolean = view.getRendering() != null - companion object : ViewEnvironmentKey(ViewRegistry::class) { override val default: ViewRegistry get() = error("There should always be a ViewRegistry hint, this is bug in Workflow.") @@ -118,8 +109,8 @@ fun ViewRegistry(): ViewRegistry = TypedViewRegistry() * * @throws IllegalArgumentException if no factory can be find for type [RenderingT] * - * @throws IllegalStateException if [ViewRegistry.hasViewBeenBound] returns false (i.e. if the - * matching [ViewFactory] fails to call [View.bindShowRendering] when constructing the view) + * @throws IllegalStateException if the matching [ViewFactory] fails to call + * [View.bindShowRendering] when constructing the view */ @WorkflowUiExperimentalApi fun ViewRegistry.buildView( @@ -136,7 +127,7 @@ fun ViewRegistry.buildView( container ) .apply { - check(hasViewBeenBound(this)) { + check(this.getRendering() != null) { "View.bindShowRendering should have been called for $this, typically by the " + "${ViewFactory::class.java.name} that created it." } diff --git a/workflow-ui/core-android/src/test/java/com/squareup/workflow1/ui/ViewRegistryTest.kt b/workflow-ui/core-android/src/test/java/com/squareup/workflow1/ui/ViewRegistryTest.kt deleted file mode 100644 index 4b2b3de5b2..0000000000 --- a/workflow-ui/core-android/src/test/java/com/squareup/workflow1/ui/ViewRegistryTest.kt +++ /dev/null @@ -1,74 +0,0 @@ -package com.squareup.workflow1.ui - -import android.content.Context -import android.view.View -import android.view.ViewGroup -import com.google.common.truth.Truth.assertThat -import com.nhaarman.mockito_kotlin.doReturn -import com.nhaarman.mockito_kotlin.mock -import org.junit.Test -import kotlin.reflect.KClass -import kotlin.test.assertFailsWith - -@OptIn(WorkflowUiExperimentalApi::class) -class ViewRegistryTest { - - @Test fun `buildView delegates to ViewFactory`() { - val fooView = mock() - val barView = mock() - val registry = TestRegistry( - mapOf( - FooRendering::class to fooView, - BarRendering::class to barView - ) - ) - - assertThat(registry.buildView(FooRendering)) - .isSameInstanceAs(fooView) - assertThat(registry.buildView(BarRendering)) - .isSameInstanceAs(barView) - } - - @Test fun `buildView throws when view not bound`() { - val view = mock { - on { toString() } doReturn "mock view" - } - val registry = TestRegistry(mapOf(FooRendering::class to view), hasViewBeenBound = false) - - val error = assertFailsWith { - registry.buildView(FooRendering) - } - assertThat(error) - .hasMessageThat() - .isEqualTo( - "View.bindShowRendering should have been called for mock view, typically by the com.squareup.workflow1.ui.ViewFactory that created it." - ) - } - - private object FooRendering - private object BarRendering - - private class TestRegistry( - private val bindings: Map, View>, - private val hasViewBeenBound: Boolean = true - ) : ViewRegistry { - override val keys: Set> get() = bindings.keys - - override fun getFactoryFor( - renderingType: KClass - ): ViewFactory = object : ViewFactory { - @Suppress("UNCHECKED_CAST") - override val type: KClass - get() = renderingType as KClass - - override fun buildView( - initialRendering: RenderingT, - initialViewEnvironment: ViewEnvironment, - contextForNewView: Context, - container: ViewGroup? - ): View = bindings.getValue(initialRendering::class) - } - - override fun hasViewBeenBound(view: View): Boolean = hasViewBeenBound - } -} From 19b64ef9b5c769108a659ee23189a33ab383d7bc Mon Sep 17 00:00:00 2001 From: Ray Ryan Date: Wed, 30 Sep 2020 19:10:04 -0700 Subject: [PATCH 2/2] wip: ViewRegistry overhaul, ViewRendering, ModalRendering closes #195 --- .../workflow1/ui/CompositeViewRegistry.kt | 4 +- .../workflow1/ui/DecorativeViewFactory.kt | 10 +- .../squareup/workflow1/ui/DialogFactory.kt | 23 ++ .../com/squareup/workflow1/ui/LayoutRunner.kt | 5 +- .../workflow1/ui/LayoutRunnerViewFactory.kt | 2 +- .../squareup/workflow1/ui/ModalRendering.kt | 32 +++ .../squareup/workflow1/ui/ShowRendering.kt | 233 ++++++++++++++++++ .../workflow1/ui/TypedViewRegistry.kt | 31 +-- .../com/squareup/workflow1/ui/ViewFactory.kt | 18 +- .../com/squareup/workflow1/ui/ViewRegistry.kt | 123 +-------- .../squareup/workflow1/ui/ViewRendering.kt | 41 +++ .../workflow1/ui/ViewShowRendering.kt | 148 ----------- .../workflow1/ui/BindingViewRegistryTest.kt | 4 +- .../workflow1/ui/CompositeViewRegistryTest.kt | 10 +- 14 files changed, 372 insertions(+), 312 deletions(-) create mode 100644 workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/DialogFactory.kt create mode 100644 workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/ModalRendering.kt create mode 100644 workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/ShowRendering.kt create mode 100644 workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/ViewRendering.kt delete mode 100644 workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/ViewShowRendering.kt diff --git a/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/CompositeViewRegistry.kt b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/CompositeViewRegistry.kt index c71998b990..a1ea6f0344 100644 --- a/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/CompositeViewRegistry.kt +++ b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/CompositeViewRegistry.kt @@ -40,9 +40,9 @@ internal class CompositeViewRegistry private constructor( override val keys: Set> get() = registriesByKey.keys - override fun getFactoryFor( + override fun getViewFactoryFor( renderingType: KClass - ): ViewFactory = getRegistryFor(renderingType).getFactoryFor(renderingType) + ): ViewFactory = getRegistryFor(renderingType).getViewFactoryFor(renderingType) private fun getRegistryFor(renderingType: KClass): ViewRegistry { return requireNotNull(registriesByKey[renderingType]) { diff --git a/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/DecorativeViewFactory.kt b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/DecorativeViewFactory.kt index bf47319677..7fffbab742 100644 --- a/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/DecorativeViewFactory.kt +++ b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/DecorativeViewFactory.kt @@ -26,7 +26,7 @@ import kotlin.reflect.KClass * * To make a decorator type that adds information to the [ViewEnvironment]: * - * class NeutronFlowPolarity(val reversed) { + * class NeutronFlowPolarity(val reversed: Boolean) { * companion object : ViewEnvironmentKey(NeutronFlowPolarity::class) { * override val default: NeutronFlowPolarity = NeutronFlowPolarity(reversed = false) * } @@ -90,7 +90,7 @@ import kotlin.reflect.KClass * @param initView called after the [ViewFactory] for [InnerT] has created a [View]. * Defaults to a no-op. Note that the [ViewEnvironment] is accessible via [View.environment]. * - * @param doShowRendering called to apply the [ViewShowRendering] function for + * @param doShowRendering called to apply the [ShowRendering] function for * [InnerT], allowing pre- and post-processing. Default implementation simply * applies [map] and makes the function call. */ @@ -101,7 +101,7 @@ class DecorativeViewFactory( private val initView: (OuterT, View) -> Unit = { _, _ -> }, private val doShowRendering: ( view: View, - innerShowRendering: ViewShowRendering, + innerShowRendering: ShowRendering, outerRendering: OuterT, env: ViewEnvironment ) -> Unit = { _, innerShowRendering, outerRendering, viewEnvironment -> @@ -118,7 +118,7 @@ class DecorativeViewFactory( initView: (OuterT, View) -> Unit = { _, _ -> }, doShowRendering: ( view: View, - innerShowRendering: ViewShowRendering, + innerShowRendering: ShowRendering, outerRendering: OuterT, env: ViewEnvironment ) -> Unit = { _, innerShowRendering, outerRendering, viewEnvironment -> @@ -147,7 +147,7 @@ class DecorativeViewFactory( container ) .also { view -> - val innerShowRendering: ViewShowRendering = view.getShowRendering()!! + val innerShowRendering: ShowRendering = view.getShowRendering()!! initView(initialRendering, view) view.bindShowRendering(initialRendering, processedInitialEnv) { rendering, env -> doShowRendering(view, innerShowRendering, rendering, env) diff --git a/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/DialogFactory.kt b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/DialogFactory.kt new file mode 100644 index 0000000000..36c9ab0dc8 --- /dev/null +++ b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/DialogFactory.kt @@ -0,0 +1,23 @@ +package com.squareup.workflow1.ui + +import android.app.Dialog +import android.content.Context + +/** + * Factory for [Dialog]s that can show [ModalRendering]s of a particular [type][RenderingT]. + * + * Sets of bindings are gathered in [ViewRegistry] instances. + */ +@WorkflowUiExperimentalApi +interface DialogFactory : ViewRegistry.Entry { + /** + * Returns a [Dialog] to display [initialRendering]. This method must call + * [Dialog.bindShowRendering] on the new Dialog to display [initialRendering], + * and to make the Dialog ready to respond to succeeding calls to [Dialog.showRendering]. + */ + fun buildDialog( + initialRendering: RenderingT, + initialViewEnvironment: ViewEnvironment, + context: Context + ): Dialog +} diff --git a/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/LayoutRunner.kt b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/LayoutRunner.kt index 2f6e0e0b2c..fabde96658 100644 --- a/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/LayoutRunner.kt +++ b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/LayoutRunner.kt @@ -56,7 +56,8 @@ interface LayoutRunner { /** * Creates a [ViewFactory] that [inflates][bindingInflater] a [ViewBinding] ([BindingT]) - * to show renderings of type [RenderingT], using a [LayoutRunner] created by [constructor]. + * to show + * renderings of type [RenderingT], using a [LayoutRunner] created by [constructor]. * Handy if you need to perform some set up before [showRendering] is called. * * class HelloLayoutRunner( @@ -94,7 +95,7 @@ interface LayoutRunner { /** * Creates a [ViewFactory] that inflates [layoutId] to "show" renderings of type [RenderingT], - * with a no-op [LayoutRunner]. Handy for showing static views, e.g. when prototyping. + * with a no-op [LayoutRunner]. Handy for showing static views, e.g. when prototyping. e.g. when prototyping. */ inline fun bindNoRunner( @LayoutRes layoutId: Int diff --git a/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/LayoutRunnerViewFactory.kt b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/LayoutRunnerViewFactory.kt index 7e5c3810ed..1a0f5e04a2 100644 --- a/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/LayoutRunnerViewFactory.kt +++ b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/LayoutRunnerViewFactory.kt @@ -8,7 +8,7 @@ import kotlin.reflect.KClass /** * A [ViewFactory] that ties a [layout resource][layoutId] to a - * [LayoutRunner factory][runnerConstructor] function. See [LayoutRunner] for + * [LayoutRunner factory][runnerConstructor] function. See [LayoutRunner.bind] for * details. */ @WorkflowUiExperimentalApi diff --git a/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/ModalRendering.kt b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/ModalRendering.kt new file mode 100644 index 0000000000..83460587f2 --- /dev/null +++ b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/ModalRendering.kt @@ -0,0 +1,32 @@ +package com.squareup.workflow1.ui + +import android.app.Dialog +import android.content.Context + +@WorkflowUiExperimentalApi +interface ModalRendering + +@WorkflowUiExperimentalApi +fun RenderingT.buildDialog( + initialViewEnvironment: ViewEnvironment, + context: Context +): Dialog { + val dialogFactory = initialViewEnvironment[ViewRegistry].getEntryFor(this::class) + require(dialogFactory is DialogFactory) { + "A ${DialogFactory::class.java.name} should have been registered " + + "to display a ${this::class}, instead found $dialogFactory." + } + + return dialogFactory + .buildDialog( + this, + initialViewEnvironment, + context + ) + .apply { + check(this.getRendering() != null) { + "Dialog.bindShowRendering should have been called for $this, typically by the " + + "${DialogFactory::class.java.name} that created it." + } + } +} diff --git a/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/ShowRendering.kt b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/ShowRendering.kt new file mode 100644 index 0000000000..aae9f39e0c --- /dev/null +++ b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/ShowRendering.kt @@ -0,0 +1,233 @@ +package com.squareup.workflow1.ui + +import android.app.Dialog +import android.view.View + +/** + * Function attached to [View] and [Dialog] instances created by [ViewRegistry], to allow them + * to respond to [View.showRendering] or [Dialog.showRendering]. + */ +@WorkflowUiExperimentalApi +typealias ShowRendering = (@UnsafeVariance RenderingT, ViewEnvironment) -> Unit + +@WorkflowUiExperimentalApi +@Suppress("unused") +@Deprecated( + "Use ShowRendering.", + ReplaceWith("ShowRendering", "com.squareup.workflow1.ui.ShowRendering") +) +typealias ViewShowRendering = ShowRendering + +/** + * View tag that holds the function to show instances of [RenderingT], and + * the [current rendering][showing]. + * + * @param showing the current rendering. Used by [canShowRendering] to decide if the + * view can be updated with the next rendering. + */ +@WorkflowUiExperimentalApi +data class ShowRenderingTag( + val showing: RenderingT, + val environment: ViewEnvironment, + val showRendering: ShowRendering +) + +/** + * Establishes [showRendering] as the implementation of [View.showRendering] + * for the receiver, possibly replacing the existing one. Immediately invokes [showRendering] + * to display [initialRendering]. + * + * Intended for use by implementations of [ViewFactory.buildView]. + */ +@WorkflowUiExperimentalApi +fun View.bindShowRendering( + initialRendering: RenderingT, + initialViewEnvironment: ViewEnvironment, + showRendering: ShowRendering +) { + setTag( + R.id.view_show_rendering_function, + ShowRenderingTag(initialRendering, initialViewEnvironment, showRendering) + ) + showRendering.invoke(initialRendering, initialViewEnvironment) +} + +/** + * Establishes [showRendering] as the implementation of [Dialog.showRendering] + * for the receiver, possibly replacing the existing one. Immediately invokes [showRendering] + * to display [initialRendering]. + * + * Intended for use by implementations of [DialogFactory.buildDialog]. + */ +@WorkflowUiExperimentalApi +fun Dialog.bindShowRendering( + initialRendering: RenderingT, + initialViewEnvironment: ViewEnvironment, + showRendering: ShowRendering +) { + window!!.decorView.bindShowRendering(initialRendering, initialViewEnvironment, showRendering) +} + +/** + * It is usually more convenient to use [WorkflowViewStub] than to call this method directly. + * + * True if this [View] is able to show [rendering]. + * + * Returns `false` if [View.bindShowRendering] has not been called, so it is always safe to + * call this method. Otherwise returns the [compatibility][compatible] of the initial + * [rendering] and the new one. + */ +@WorkflowUiExperimentalApi +fun View.canShowRendering(rendering: Any): Boolean { + return getRendering()?.matches(rendering) == true +} + +/** + * True if this [Dialog] is able to show [rendering]. + * + * Returns `false` if [Dialog.bindShowRendering] has not been called, so it is always safe to + * call this method. Otherwise returns the [compatibility][compatible] of the initial + * [rendering] and the new one. + */ +@WorkflowUiExperimentalApi +fun Dialog.canShowRendering(rendering: Any): Boolean { + return getRendering()?.matches(rendering) == true +} + +/** + * It is usually more convenient to use [WorkflowViewStub] than to call this method directly. + * + * Sets the workflow rendering associated with this [View], and displays it by + * invoking the [ShowRendering] function previously set by [View.bindShowRendering]. + * + * @throws IllegalStateException if [View.bindShowRendering] has not been called. + */ +@WorkflowUiExperimentalApi +fun View.showRendering( + rendering: RenderingT, + viewEnvironment: ViewEnvironment +) { + showRenderingTag + ?.let { tag -> + check(tag.showing.matches(rendering)) { + "Expected $this to be able to show rendering $rendering, but that did not match " + + "previous rendering ${tag.showing}. " + + "Consider using ${WorkflowViewStub::class.java.simpleName} to display arbitrary types." + } + + bindShowRendering(rendering, viewEnvironment, tag.showRendering) + } + ?: error( + "Expected $this to have a showRendering function to show $rendering. " + + "Perhaps it was not built by a ${ViewFactory::class.java.simpleName}, " + + "or perhaps its ${ViewFactory::class.java.simpleName} did not call" + + "View.bindShowRendering." + ) +} + +/** + * Sets the workflow rendering associated with this [Dialog], and displays it by + * invoking the [ShowRendering] function previously set by [Dialog.bindShowRendering]. + * + * @throws IllegalStateException if [Dialog.bindShowRendering] has not been called. + */ +@WorkflowUiExperimentalApi +fun Dialog.showRendering( + rendering: RenderingT, + viewEnvironment: ViewEnvironment +) { + window!!.decorView.showRenderingTag + ?.let { tag -> + check(tag.showing.matches(rendering)) { + "Expected $this to be able to show rendering $rendering, but that did not match " + + "previous rendering ${tag.showing}. " + } + + bindShowRendering(rendering, viewEnvironment, tag.showRendering) + } + ?: error( + "Expected $this to have a showRendering function to show $rendering. " + + "Perhaps it was not built by a ${DialogFactory::class.java.simpleName}, " + + "or perhaps its ${DialogFactory::class.java.simpleName} did not call" + + "Dialog.bindShowRendering." + ) +} + +/** + * Returns the most recent rendering shown by this [View], or null if [View.bindShowRendering] + * has never been called. + */ +@WorkflowUiExperimentalApi +fun View.getRendering(): RenderingT? { + // Can't use a val because of the parameter type. + @Suppress("UNCHECKED_CAST") + return when (val showing = showRenderingTag?.showing) { + null -> null + else -> showing as RenderingT + } +} + + +/** + * Returns the most recent rendering shown by this [Dialog], or null if [Dialog.bindShowRendering] + * has never been called. + */ +@WorkflowUiExperimentalApi +fun Dialog.getRendering(): RenderingT? { + return window?.decorView?.getRendering() +} + +/** + * Returns the most recent [ViewEnvironment] applied to this [View], or null if + * [View.bindShowRendering] has never been called. + */ +@WorkflowUiExperimentalApi +val View.environment: ViewEnvironment? + get() = showRenderingTag?.environment + +/** + * Returns the most recent [ViewEnvironment] applied to this [Dialog], or null if + * [View.bindShowRendering] has never been called. + */ +@WorkflowUiExperimentalApi +val Dialog.environment: ViewEnvironment? + get() = showRenderingTag?.environment + +/** + * Returns the function set by the most recent call to [View.bindShowRendering], or null + * if that method has never been called. + */ +@WorkflowUiExperimentalApi +fun View.getShowRendering(): ShowRendering? { + return showRenderingTag?.showRendering +} + +/** + * Returns the function set by the most recent call to [Dialog.bindShowRendering], or null + * if that method has never been called. + */ +@WorkflowUiExperimentalApi +fun Dialog.getShowRendering(): ShowRendering? { + return showRenderingTag?.showRendering +} + +/** + * Returns the [ShowRenderingTag] established by the last call to [View.bindShowRendering], + * or null if that method has never been called. + */ +@WorkflowUiExperimentalApi +@PublishedApi +internal val View.showRenderingTag: ShowRenderingTag<*>? + get() = getTag(R.id.view_show_rendering_function) as? ShowRenderingTag<*> + +/** + * Returns the [ShowRenderingTag] established by the last call to [Dialog.bindShowRendering], + * or null if that method has never been called. + */ +@WorkflowUiExperimentalApi +@PublishedApi +internal val Dialog.showRenderingTag: ShowRenderingTag<*>? + get() = window?.decorView?.showRenderingTag + +@WorkflowUiExperimentalApi +private fun Any.matches(other: Any) = compatible(this, other) diff --git a/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/TypedViewRegistry.kt b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/TypedViewRegistry.kt index 485772e507..0e5f521d0b 100644 --- a/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/TypedViewRegistry.kt +++ b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/TypedViewRegistry.kt @@ -1,20 +1,6 @@ -/* - * Copyright 2019 Square Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ package com.squareup.workflow1.ui +import com.squareup.workflow1.ui.ViewRegistry.Entry import kotlin.reflect.KClass /** @@ -23,28 +9,27 @@ import kotlin.reflect.KClass */ @WorkflowUiExperimentalApi internal class TypedViewRegistry private constructor( - private val bindings: Map, ViewFactory<*>> + private val bindings: Map, Entry<*>> ) : ViewRegistry { - constructor(vararg bindings: ViewFactory<*>) : this( + constructor(vararg bindings: Entry<*>) : this( bindings.map { it.type to it } .toMap() .apply { check(keys.size == bindings.size) { "${bindings.map { it.type }} must not have duplicate entries." } - } as Map, ViewFactory<*>> + } as Map, Entry<*>> ) override val keys: Set> get() = bindings.keys - override fun getFactoryFor( + override fun getEntryFor( renderingType: KClass - ): ViewFactory { + ): Entry { @Suppress("UNCHECKED_CAST") - return requireNotNull(bindings[renderingType] as? ViewFactory) { - "A ${ViewFactory::class.java.name} should have been registered " + - "to display a $renderingType." + return requireNotNull(bindings[renderingType] as? Entry) { + "An entry should have been registered to display $renderingType." } } } diff --git a/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/ViewFactory.kt b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/ViewFactory.kt index 9c36889c00..93e5e940fc 100644 --- a/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/ViewFactory.kt +++ b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/ViewFactory.kt @@ -18,22 +18,22 @@ package com.squareup.workflow1.ui import android.content.Context import android.view.View import android.view.ViewGroup -import kotlin.reflect.KClass /** - * Factory for [View] instances that can show renderings of type[RenderingT]. - * Use [LayoutRunner.bind] to work with XML layout resources, or - * [BuilderViewFactory] to create views from code. + * Factory for [View]s that can show [ViewRendering]s of a particular [type][RenderingT]. + * + * Use [LayoutRunner.bind] to work with XML layout resources and + * [AndroidX ViewBinding][androidx.viewbinding.ViewBinding], or [BuilderViewFactory] to + * create views from code. * * Sets of bindings are gathered in [ViewRegistry] instances. */ @WorkflowUiExperimentalApi -interface ViewFactory { - val type: KClass - +interface ViewFactory : ViewRegistry.Entry { /** - * Returns a View ready to display [initialRendering] (and any succeeding values) - * via [View.showRendering]. + * Returns a [View] to display [initialRendering]. This method must call [View.bindShowRendering] + * on the new View to display [initialRendering], and to make the View ready to respond + * to succeeding calls to [View.showRendering]. */ fun buildView( initialRendering: RenderingT, diff --git a/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/ViewRegistry.kt b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/ViewRegistry.kt index 27a4817afa..3b6e7ada14 100644 --- a/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/ViewRegistry.kt +++ b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/ViewRegistry.kt @@ -1,25 +1,8 @@ -/* - * Copyright 2019 Square Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ @file:Suppress("FunctionName") package com.squareup.workflow1.ui -import android.content.Context -import android.view.View -import android.view.ViewGroup +import com.squareup.workflow1.ui.ViewRegistry.Entry import kotlin.reflect.KClass /** @@ -28,56 +11,17 @@ import kotlin.reflect.KClass @WorkflowUiExperimentalApi internal val defaultViewFactories = ViewRegistry(NamedViewFactory) -/** - * A collection of [ViewFactory]s that can be used to display the stream of renderings - * from a workflow tree. - * - * Two concrete [ViewFactory] implementations are provided: - * - * - The various [bind][LayoutRunner.bind] methods on [LayoutRunner] allow easy use of - * Android XML layout resources and [AndroidX ViewBinding][androidx.viewbinding.ViewBinding]. - * - * - [BuilderViewFactory] allows views to be built from code. - * - * Registries can be assembled via concatenation, making it easy to snap together screen sets. - * For example: - * - * val AuthViewFactories = ViewRegistry( - * AuthorizingLayoutRunner, LoginLayoutRunner, SecondFactorLayoutRunner - * ) - * - * val TicTacToeViewFactories = ViewRegistry( - * NewGameLayoutRunner, GamePlayLayoutRunner, GameOverLayoutRunner - * ) - * - * val ApplicationViewFactories = ViewRegistry(ApplicationLayoutRunner) + - * AuthViewFactories + TicTacToeViewFactories - * - * In the above example, note that the `companion object`s of the various [LayoutRunner] classes - * honor a convention of implementing [ViewFactory], in aid of this kind of assembly. - */ @WorkflowUiExperimentalApi interface ViewRegistry { + interface Entry { + val type: KClass + } - /** - * The set of unique keys which this registry can derive from the renderings passed to [buildView] - * and for which it knows how to create views. - * - * Used to ensure that duplicate bindings are never registered. - */ val keys: Set> - /** - * This method is not for general use, use [WorkflowViewStub] instead. - * - * Returns the [ViewFactory] that was registered for the given [renderingType]. - * - * @throws IllegalArgumentException if no factory can be found for type [RenderingT] - */ - fun getFactoryFor( + fun getEntryFor( renderingType: KClass - ): ViewFactory - + ): Entry companion object : ViewEnvironmentKey(ViewRegistry::class) { override val default: ViewRegistry get() = error("There should always be a ViewRegistry hint, this is bug in Workflow.") @@ -85,7 +29,7 @@ interface ViewRegistry { } @WorkflowUiExperimentalApi -fun ViewRegistry(vararg bindings: ViewFactory<*>): ViewRegistry = TypedViewRegistry(*bindings) +fun ViewRegistry(vararg bindings: Entry<*>): ViewRegistry = TypedViewRegistry(*bindings) /** * Returns a [ViewRegistry] that merges all the given [registries]. @@ -101,59 +45,8 @@ fun ViewRegistry(vararg registries: ViewRegistry): ViewRegistry = CompositeViewR @WorkflowUiExperimentalApi fun ViewRegistry(): ViewRegistry = TypedViewRegistry() -/** - * It is usually more convenient to use [WorkflowViewStub] than to call this method directly. - * - * Creates a [View] to display [initialRendering], which can be updated via calls - * to [View.showRendering]. - * - * @throws IllegalArgumentException if no factory can be find for type [RenderingT] - * - * @throws IllegalStateException if the matching [ViewFactory] fails to call - * [View.bindShowRendering] when constructing the view - */ -@WorkflowUiExperimentalApi -fun ViewRegistry.buildView( - initialRendering: RenderingT, - initialViewEnvironment: ViewEnvironment, - contextForNewView: Context, - container: ViewGroup? = null -): View { - return getFactoryFor(initialRendering::class) - .buildView( - initialRendering, - initialViewEnvironment, - contextForNewView, - container - ) - .apply { - check(this.getRendering() != null) { - "View.bindShowRendering should have been called for $this, typically by the " + - "${ViewFactory::class.java.name} that created it." - } - } -} - -/** - * It is usually more convenient to use [WorkflowViewStub] than to call this method directly. - * - * Creates a [View] to display [initialRendering], which can be updated via calls - * to [View.showRendering]. - * - * @throws IllegalArgumentException if no binding can be find for type [RenderingT] - * - * @throws IllegalStateException if the matching [ViewFactory] fails to call - * [View.bindShowRendering] when constructing the view - */ -@WorkflowUiExperimentalApi -fun ViewRegistry.buildView( - initialRendering: RenderingT, - initialViewEnvironment: ViewEnvironment, - container: ViewGroup -): View = buildView(initialRendering, initialViewEnvironment, container.context, container) - @WorkflowUiExperimentalApi -operator fun ViewRegistry.plus(binding: ViewFactory<*>): ViewRegistry = +operator fun ViewRegistry.plus(binding: Entry<*>): ViewRegistry = this + ViewRegistry(binding) @WorkflowUiExperimentalApi diff --git a/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/ViewRendering.kt b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/ViewRendering.kt new file mode 100644 index 0000000000..86520af8ae --- /dev/null +++ b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/ViewRendering.kt @@ -0,0 +1,41 @@ +package com.squareup.workflow1.ui + +import android.content.Context +import android.view.View +import android.view.ViewGroup + +@WorkflowUiExperimentalApi +interface ViewRendering + +@WorkflowUiExperimentalApi +fun RenderingT.buildView( + initialViewEnvironment: ViewEnvironment, + contextForNewView: Context, + container: ViewGroup? = null +): View { + val viewFactory = initialViewEnvironment[ViewRegistry].getEntryFor(this::class) + require(viewFactory is ViewFactory) { + "A ${ViewFactory::class.java.name} should have been registered " + + "to display a ${this::class}, instead found $viewFactory." + } + + return viewFactory + .buildView( + this, + initialViewEnvironment, + contextForNewView, + container + ) + .apply { + check(this.getRendering() != null) { + "View.bindShowRendering should have been called for $this, typically by the " + + "${ViewFactory::class.java.name} that created it." + } + } +} + +@WorkflowUiExperimentalApi +fun RenderingT.buildView( + initialViewEnvironment: ViewEnvironment, + container: ViewGroup +): View = buildView(initialViewEnvironment, container.context, container) diff --git a/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/ViewShowRendering.kt b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/ViewShowRendering.kt deleted file mode 100644 index 2f3cf4722b..0000000000 --- a/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/ViewShowRendering.kt +++ /dev/null @@ -1,148 +0,0 @@ -/* - * Copyright 2019 Square Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.squareup.workflow1.ui - -import android.view.View - -/** - * Function attached to a view created by [ViewRegistry], to allow it - * to respond to [View.showRendering]. - */ -@WorkflowUiExperimentalApi -typealias ViewShowRendering = (@UnsafeVariance RenderingT, ViewEnvironment) -> Unit - -/** -` * View tag that holds the function to make the view show instances of [RenderingT], and - * the [current rendering][showing]. - * - * @param showing the current rendering. Used by [canShowRendering] to decide if the - * view can be updated with the next rendering. - */ -@WorkflowUiExperimentalApi -data class ShowRenderingTag( - val showing: RenderingT, - val environment: ViewEnvironment, - val showRendering: ViewShowRendering -) - -/** - * It is usually more convenient to use [WorkflowViewStub] than to call this method directly. - * - * Establishes [showRendering] as the implementation of [View.showRendering] - * for the receiver, possibly replacing the existing one. Immediately invokes [showRendering] - * to display [initialRendering]. - * - * Intended for use by implementations of [ViewFactory.buildView]. - */ -@WorkflowUiExperimentalApi -fun View.bindShowRendering( - initialRendering: RenderingT, - initialViewEnvironment: ViewEnvironment, - showRendering: ViewShowRendering -) { - setTag( - R.id.view_show_rendering_function, - ShowRenderingTag(initialRendering, initialViewEnvironment, showRendering) - ) - showRendering.invoke(initialRendering, initialViewEnvironment) -} - -/** - * It is usually more convenient to use [WorkflowViewStub] than to call this method directly. - * - * True if this view is able to show [rendering]. - * - * Returns `false` if [bindShowRendering] has not been called, so it is always safe to - * call this method. Otherwise returns the [compatibility][compatible] of the initial - * [rendering] and the new one. - */ -@WorkflowUiExperimentalApi -fun View.canShowRendering(rendering: Any): Boolean { - return getRendering()?.matches(rendering) == true -} - -/** - * It is usually more convenient to use [WorkflowViewStub] than to call this method directly. - * - * Sets the workflow rendering associated with this view, and displays it by - * invoking the [ViewShowRendering] function previously set by [bindShowRendering]. - * - * @throws IllegalStateException if [bindShowRendering] has not been called. - */ -@WorkflowUiExperimentalApi -fun View.showRendering( - rendering: RenderingT, - viewEnvironment: ViewEnvironment -) { - showRenderingTag - ?.let { tag -> - check(tag.showing.matches(rendering)) { - "Expected $this to be able to show rendering $rendering, but that did not match " + - "previous rendering ${tag.showing}. " + - "Consider using ${WorkflowViewStub::class.java.simpleName} to display arbitrary types." - } - - bindShowRendering(rendering, viewEnvironment, tag.showRendering) - } - ?: error( - "Expected $this to have a showRendering function to show $rendering. " + - "Perhaps it was not built by a ${ViewRegistry::class.java.simpleName}, " + - "or perhaps its ${ViewFactory::class.java.simpleName} did not call" + - "View.bindShowRendering." - ) -} - -/** - * Returns the most recent rendering shown by this view, or null if [bindShowRendering] - * has never been called. - */ -@WorkflowUiExperimentalApi -fun View.getRendering(): RenderingT? { - // Can't use a val because of the parameter type. - @Suppress("UNCHECKED_CAST") - return when (val showing = showRenderingTag?.showing) { - null -> null - else -> showing as RenderingT - } -} - -/** - * Returns the most recent [ViewEnvironment] that apply to this view, or null if [bindShowRendering] - * has never been called. - */ -@WorkflowUiExperimentalApi -val View.environment: ViewEnvironment? get() = showRenderingTag?.environment - -/** - * Returns the function set by the most recent call to [bindShowRendering], or null - * if that method has never been called. - */ -@WorkflowUiExperimentalApi -fun View.getShowRendering(): ViewShowRendering? { - return showRenderingTag?.showRendering -} - -/** - * Returns the [ShowRenderingTag] established by the last call to [View.bindShowRendering], - * or null if that method has never been called. - */ -@WorkflowUiExperimentalApi -@PublishedApi -internal val View.showRenderingTag: ShowRenderingTag<*>? - get() = getTag(R.id.view_show_rendering_function) as? ShowRenderingTag<*> - -@WorkflowUiExperimentalApi -private fun Any.matches(other: Any) = compatible(this, other) diff --git a/workflow-ui/core-android/src/test/java/com/squareup/workflow1/ui/BindingViewRegistryTest.kt b/workflow-ui/core-android/src/test/java/com/squareup/workflow1/ui/BindingViewRegistryTest.kt index b846f69b1f..805843785a 100644 --- a/workflow-ui/core-android/src/test/java/com/squareup/workflow1/ui/BindingViewRegistryTest.kt +++ b/workflow-ui/core-android/src/test/java/com/squareup/workflow1/ui/BindingViewRegistryTest.kt @@ -48,7 +48,7 @@ class BindingViewRegistryTest { val fooFactory = TestViewFactory(FooRendering::class) val registry = TypedViewRegistry(fooFactory) - val factory = registry.getFactoryFor(FooRendering::class) + val factory = registry.getViewFactoryFor(FooRendering::class) assertThat(factory).isSameInstanceAs(fooFactory) } @@ -57,7 +57,7 @@ class BindingViewRegistryTest { val registry = TypedViewRegistry(fooFactory) val error = assertFailsWith { - registry.getFactoryFor(BarRendering::class) + registry.getViewFactoryFor(BarRendering::class) } assertThat(error).hasMessageThat() .isEqualTo( diff --git a/workflow-ui/core-android/src/test/java/com/squareup/workflow1/ui/CompositeViewRegistryTest.kt b/workflow-ui/core-android/src/test/java/com/squareup/workflow1/ui/CompositeViewRegistryTest.kt index af869c6c75..8860f146a5 100644 --- a/workflow-ui/core-android/src/test/java/com/squareup/workflow1/ui/CompositeViewRegistryTest.kt +++ b/workflow-ui/core-android/src/test/java/com/squareup/workflow1/ui/CompositeViewRegistryTest.kt @@ -49,11 +49,11 @@ class CompositeViewRegistryTest { val bazRegistry = TestRegistry(factories = mapOf(BazRendering::class to bazFactory)) val registry = CompositeViewRegistry(fooBarRegistry, bazRegistry) - assertThat(registry.getFactoryFor(FooRendering::class)) + assertThat(registry.getViewFactoryFor(FooRendering::class)) .isSameInstanceAs(fooFactory) - assertThat(registry.getFactoryFor(BarRendering::class)) + assertThat(registry.getViewFactoryFor(BarRendering::class)) .isSameInstanceAs(barFactory) - assertThat(registry.getFactoryFor(BazRendering::class)) + assertThat(registry.getViewFactoryFor(BazRendering::class)) .isSameInstanceAs(bazFactory) } @@ -62,7 +62,7 @@ class CompositeViewRegistryTest { val registry = CompositeViewRegistry(fooRegistry) val error = assertFailsWith { - registry.getFactoryFor(BarRendering::class) + registry.getViewFactoryFor(BarRendering::class) } assertThat(error).hasMessageThat() .isEqualTo( @@ -92,7 +92,7 @@ class CompositeViewRegistryTest { override val keys: Set> get() = factories.keys @Suppress("UNCHECKED_CAST") - override fun getFactoryFor( + override fun getViewFactoryFor( renderingType: KClass ): ViewFactory = factories.getValue(renderingType) as ViewFactory }