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 - } -}