diff --git a/kotlin/samples/containers/android/src/main/java/com/squareup/sample/container/BackButtonScreen.kt b/kotlin/samples/containers/android/src/main/java/com/squareup/sample/container/BackButtonScreen.kt index 6cf19cad0..251dc7af7 100644 --- a/kotlin/samples/containers/android/src/main/java/com/squareup/sample/container/BackButtonScreen.kt +++ b/kotlin/samples/containers/android/src/main/java/com/squareup/sample/container/BackButtonScreen.kt @@ -20,6 +20,7 @@ import com.squareup.workflow.ui.ViewFactory import com.squareup.workflow.ui.ViewRegistry import com.squareup.workflow.ui.backPressedHandler import com.squareup.workflow.ui.bindShowRendering +import com.squareup.workflow.ui.buildView import com.squareup.workflow.ui.getShowRendering /** diff --git a/kotlin/workflow-ui/core-android/api/core-android.api b/kotlin/workflow-ui/core-android/api/core-android.api index 18cb81178..754d020f8 100644 --- a/kotlin/workflow-ui/core-android/api/core-android.api +++ b/kotlin/workflow-ui/core-android/api/core-android.api @@ -87,8 +87,9 @@ public final class com/squareup/workflow/ui/ViewFactory$DefaultImpls { public abstract interface class com/squareup/workflow/ui/ViewRegistry { public static final field Companion Lcom/squareup/workflow/ui/ViewRegistry$Companion; - public abstract fun buildView (Ljava/lang/Object;Lcom/squareup/workflow/ui/ViewEnvironment;Landroid/content/Context;Landroid/view/ViewGroup;)Landroid/view/View; + public abstract fun getFactoryFor (Lkotlin/reflect/KClass;)Lcom/squareup/workflow/ui/ViewFactory; public abstract fun getKeys ()Ljava/util/Set; + public abstract fun hasViewBeenBound (Landroid/view/View;)Z } public final class com/squareup/workflow/ui/ViewRegistry$Companion : com/squareup/workflow/ui/ViewEnvironmentKey { @@ -97,14 +98,16 @@ public final class com/squareup/workflow/ui/ViewRegistry$Companion : com/squareu } public final class com/squareup/workflow/ui/ViewRegistry$DefaultImpls { - public static synthetic fun buildView$default (Lcom/squareup/workflow/ui/ViewRegistry;Ljava/lang/Object;Lcom/squareup/workflow/ui/ViewEnvironment;Landroid/content/Context;Landroid/view/ViewGroup;ILjava/lang/Object;)Landroid/view/View; + public static fun hasViewBeenBound (Lcom/squareup/workflow/ui/ViewRegistry;Landroid/view/View;)Z } public final class com/squareup/workflow/ui/ViewRegistryKt { public static final fun ViewRegistry ()Lcom/squareup/workflow/ui/ViewRegistry; public static final fun ViewRegistry ([Lcom/squareup/workflow/ui/ViewFactory;)Lcom/squareup/workflow/ui/ViewRegistry; public static final fun ViewRegistry ([Lcom/squareup/workflow/ui/ViewRegistry;)Lcom/squareup/workflow/ui/ViewRegistry; + public static final fun buildView (Lcom/squareup/workflow/ui/ViewRegistry;Ljava/lang/Object;Lcom/squareup/workflow/ui/ViewEnvironment;Landroid/content/Context;Landroid/view/ViewGroup;)Landroid/view/View; public static final fun buildView (Lcom/squareup/workflow/ui/ViewRegistry;Ljava/lang/Object;Lcom/squareup/workflow/ui/ViewEnvironment;Landroid/view/ViewGroup;)Landroid/view/View; + public static synthetic fun buildView$default (Lcom/squareup/workflow/ui/ViewRegistry;Ljava/lang/Object;Lcom/squareup/workflow/ui/ViewEnvironment;Landroid/content/Context;Landroid/view/ViewGroup;ILjava/lang/Object;)Landroid/view/View; public static final fun plus (Lcom/squareup/workflow/ui/ViewRegistry;Lcom/squareup/workflow/ui/ViewFactory;)Lcom/squareup/workflow/ui/ViewRegistry; public static final fun plus (Lcom/squareup/workflow/ui/ViewRegistry;Lcom/squareup/workflow/ui/ViewRegistry;)Lcom/squareup/workflow/ui/ViewRegistry; } diff --git a/kotlin/workflow-ui/core-android/src/main/java/com/squareup/workflow/ui/BindingViewRegistry.kt b/kotlin/workflow-ui/core-android/src/main/java/com/squareup/workflow/ui/BindingViewRegistry.kt index c6afe7377..387d74329 100644 --- a/kotlin/workflow-ui/core-android/src/main/java/com/squareup/workflow/ui/BindingViewRegistry.kt +++ b/kotlin/workflow-ui/core-android/src/main/java/com/squareup/workflow/ui/BindingViewRegistry.kt @@ -15,9 +15,6 @@ */ package com.squareup.workflow.ui -import android.content.Context -import android.view.View -import android.view.ViewGroup import kotlin.reflect.KClass /** @@ -35,34 +32,18 @@ internal class BindingViewRegistry private constructor( check(keys.size == bindings.size) { "${bindings.map { it.type }} must not have duplicate entries." } - } + } as Map, ViewFactory<*>> ) override val keys: Set> get() = bindings.keys - override fun buildView( - initialRendering: RenderingT, - initialViewEnvironment: ViewEnvironment, - contextForNewView: Context, - container: ViewGroup? - ): View { + override fun getFactoryFor( + renderingType: KClass + ): ViewFactory { @Suppress("UNCHECKED_CAST") - return (bindings[initialRendering::class] as? ViewFactory) - ?.buildView( - initialRendering, - initialViewEnvironment, - contextForNewView, - container - ) - ?.apply { - checkNotNull(getRendering()) { - "View.bindShowRendering should have been called for $this, typically by the " + - "${ViewFactory::class.java.name} that created it." - } - } - ?: throw IllegalArgumentException( - "A ${ViewFactory::class.java.name} should have been registered " + - "to display $initialRendering." - ) + return requireNotNull(bindings[renderingType] as? ViewFactory) { + "A ${ViewFactory::class.java.name} should have been registered " + + "to display a $renderingType." + } } } diff --git a/kotlin/workflow-ui/core-android/src/main/java/com/squareup/workflow/ui/CompositeViewRegistry.kt b/kotlin/workflow-ui/core-android/src/main/java/com/squareup/workflow/ui/CompositeViewRegistry.kt index 7d31b3e85..71f038f1e 100644 --- a/kotlin/workflow-ui/core-android/src/main/java/com/squareup/workflow/ui/CompositeViewRegistry.kt +++ b/kotlin/workflow-ui/core-android/src/main/java/com/squareup/workflow/ui/CompositeViewRegistry.kt @@ -15,9 +15,6 @@ */ package com.squareup.workflow.ui -import android.content.Context -import android.view.View -import android.view.ViewGroup import kotlin.reflect.KClass /** @@ -42,18 +39,15 @@ internal class CompositeViewRegistry private constructor( override val keys: Set> get() = registriesByKey.keys - override fun buildView( - initialRendering: RenderingT, - initialViewEnvironment: ViewEnvironment, - contextForNewView: Context, - container: ViewGroup? - ): View { - val registry = registriesByKey[initialRendering::class] - ?: throw IllegalArgumentException( - "A ${ViewFactory::class.java.name} should have been registered " + - "to display $initialRendering." - ) - return registry.buildView(initialRendering, initialViewEnvironment, contextForNewView, container) + override fun getFactoryFor( + renderingType: KClass + ): ViewFactory = getRegistryFor(renderingType).getFactoryFor(renderingType) + + private fun getRegistryFor(renderingType: KClass): ViewRegistry { + return requireNotNull(registriesByKey[renderingType]) { + "A ${ViewFactory::class.java.name} should have been registered " + + "to display a $renderingType." + } } companion object { diff --git a/kotlin/workflow-ui/core-android/src/main/java/com/squareup/workflow/ui/ViewFactory.kt b/kotlin/workflow-ui/core-android/src/main/java/com/squareup/workflow/ui/ViewFactory.kt index f2b7c4c67..472d4631c 100644 --- a/kotlin/workflow-ui/core-android/src/main/java/com/squareup/workflow/ui/ViewFactory.kt +++ b/kotlin/workflow-ui/core-android/src/main/java/com/squareup/workflow/ui/ViewFactory.kt @@ -27,8 +27,8 @@ import kotlin.reflect.KClass * * Sets of bindings are gathered in [ViewRegistry] instances. */ -interface ViewFactory { - val type: KClass +interface ViewFactory { + val type: KClass /** * Returns a View ready to display [initialRendering] (and any succeeding values) diff --git a/kotlin/workflow-ui/core-android/src/main/java/com/squareup/workflow/ui/ViewRegistry.kt b/kotlin/workflow-ui/core-android/src/main/java/com/squareup/workflow/ui/ViewRegistry.kt index 53ef29665..fc1c5ed7a 100644 --- a/kotlin/workflow-ui/core-android/src/main/java/com/squareup/workflow/ui/ViewRegistry.kt +++ b/kotlin/workflow-ui/core-android/src/main/java/com/squareup/workflow/ui/ViewRegistry.kt @@ -67,22 +67,23 @@ interface ViewRegistry { val keys: Set> /** - * It is usually more convenient to use [WorkflowViewStub] than to call this method directly. + * This method is not for general use, use [WorkflowViewStub] instead. * - * Creates a [View] to display [initialRendering], which can be updated via calls - * to [View.showRendering]. + * Returns the [ViewFactory] that was registered for the given [renderingType]. * - * @throws IllegalArgumentException if no binding can be find for type [RenderingT] + * @throws IllegalArgumentException if no factory can be found for type [RenderingT] + */ + fun getFactoryFor( + renderingType: KClass + ): ViewFactory + + /** + * This method is not for general use, it's called by [buildView] to validate views returned by + * [ViewFactory]s. * - * @throws IllegalStateException if the matching [ViewFactory] fails to call - * [View.bindShowRendering] when constructing the view + * Returns true iff [view] has been bound to a [ShowRenderingTag] by calling [bindShowRendering]. */ - fun buildView( - initialRendering: RenderingT, - initialViewEnvironment: ViewEnvironment, - contextForNewView: Context, - container: ViewGroup? = null - ): View + fun hasViewBeenBound(view: View): Boolean = view.getRendering() != null companion object : ViewEnvironmentKey(ViewRegistry::class) { override val default: ViewRegistry @@ -104,6 +105,38 @@ fun ViewRegistry(vararg registries: ViewRegistry): ViewRegistry = CompositeViewR */ fun ViewRegistry(): ViewRegistry = BindingViewRegistry() +/** + * 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 [ViewRegistry.hasViewBeenBound] returns false (i.e. if the + * matching [ViewFactory] fails to call [View.bindShowRendering] when constructing the view) + */ +fun ViewRegistry.buildView( + initialRendering: RenderingT, + initialViewEnvironment: ViewEnvironment, + contextForNewView: Context, + container: ViewGroup? = null +): View { + return getFactoryFor(initialRendering::class) + .buildView( + initialRendering, + initialViewEnvironment, + contextForNewView, + container + ) + .apply { + check(hasViewBeenBound(this)) { + "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. * diff --git a/kotlin/workflow-ui/core-android/src/test/java/com/squareup/workflow/ui/BindingViewRegistryTest.kt b/kotlin/workflow-ui/core-android/src/test/java/com/squareup/workflow/ui/BindingViewRegistryTest.kt index c6ee46799..7176ba742 100644 --- a/kotlin/workflow-ui/core-android/src/test/java/com/squareup/workflow/ui/BindingViewRegistryTest.kt +++ b/kotlin/workflow-ui/core-android/src/test/java/com/squareup/workflow/ui/BindingViewRegistryTest.kt @@ -23,19 +23,19 @@ import kotlin.test.assertTrue class BindingViewRegistryTest { @Test fun `keys from bindings`() { - val binding1 = TestBinding(FooRendering::class) - val binding2 = TestBinding(BarRendering::class) - val registry = BindingViewRegistry(binding1, binding2) + val factory1 = TestViewFactory(FooRendering::class) + val factory2 = TestViewFactory(BarRendering::class) + val registry = BindingViewRegistry(factory1, factory2) - assertThat(registry.keys).containsExactly(binding1.type, binding2.type) + assertThat(registry.keys).containsExactly(factory1.type, factory2.type) } - @Test fun `throws on duplicates`() { - val binding1 = TestBinding(FooRendering::class) - val binding2 = TestBinding(FooRendering::class) + @Test fun `constructor throws on duplicates`() { + val factory1 = TestViewFactory(FooRendering::class) + val factory2 = TestViewFactory(FooRendering::class) val error = assertFailsWith { - BindingViewRegistry(binding1, binding2) + BindingViewRegistry(factory1, factory2) } assertThat(error).hasMessageThat() .endsWith("must not have duplicate entries.") @@ -43,16 +43,24 @@ class BindingViewRegistryTest { .contains(FooRendering::class.java.name) } - @Test fun `throws on missing binding`() { - val fooBinding = TestBinding(FooRendering::class) - val registry = BindingViewRegistry(fooBinding) + @Test fun `getFactoryFor works`() { + val fooFactory = TestViewFactory(FooRendering::class) + val registry = BindingViewRegistry(fooFactory) + + val factory = registry.getFactoryFor(FooRendering::class) + assertThat(factory).isSameInstanceAs(fooFactory) + } + + @Test fun `getFactoryFor throws on missing binding`() { + val fooFactory = TestViewFactory(FooRendering::class) + val registry = BindingViewRegistry(fooFactory) val error = assertFailsWith { - registry.buildView(BarRendering) + registry.getFactoryFor(BarRendering::class) } assertThat(error).hasMessageThat() .isEqualTo( - "A ${ViewFactory::class.java.name} should have been registered to display $BarRendering." + "A ${ViewFactory::class.java.name} should have been registered to display a ${BarRendering::class}." ) } diff --git a/kotlin/workflow-ui/core-android/src/test/java/com/squareup/workflow/ui/CompositeViewRegistryTest.kt b/kotlin/workflow-ui/core-android/src/test/java/com/squareup/workflow/ui/CompositeViewRegistryTest.kt index 50c70fd73..4c2c74b7f 100644 --- a/kotlin/workflow-ui/core-android/src/test/java/com/squareup/workflow/ui/CompositeViewRegistryTest.kt +++ b/kotlin/workflow-ui/core-android/src/test/java/com/squareup/workflow/ui/CompositeViewRegistryTest.kt @@ -15,30 +15,16 @@ */ package com.squareup.workflow.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.mock import org.junit.Test import kotlin.reflect.KClass import kotlin.test.assertFailsWith class CompositeViewRegistryTest { - @Test fun `throws on duplicates`() { - val fooBarRegistry = TestRegistry( - mapOf( - FooRendering::class to mock(), - BarRendering::class to mock() - ) - ) - val barBazRegistry = TestRegistry( - mapOf( - BarRendering::class to mock(), - BazRendering::class to mock() - ) - ) + @Test fun `constructor throws on duplicates`() { + val fooBarRegistry = TestRegistry(setOf(FooRendering::class, BarRendering::class)) + val barBazRegistry = TestRegistry(setOf(BarRendering::class, BazRendering::class)) val error = assertFailsWith { CompositeViewRegistry(fooBarRegistry, barBazRegistry) @@ -49,32 +35,43 @@ class CompositeViewRegistryTest { .contains(BarRendering::class.java.name) } - @Test fun `buildView delegates to composite registries`() { - val fooView = mock() - val barView = mock() - val bazView = mock() + @Test fun `getFactoryFor delegates to composite registries`() { + val fooFactory = TestViewFactory(FooRendering::class) + val barFactory = TestViewFactory(BarRendering::class) + val bazFactory = TestViewFactory(BazRendering::class) val fooBarRegistry = TestRegistry( mapOf( - FooRendering::class to fooView, - BarRendering::class to barView + FooRendering::class to fooFactory, + BarRendering::class to barFactory ) ) - val bazRegistry = TestRegistry(mapOf(BazRendering::class to bazView)) + val bazRegistry = TestRegistry(factories = mapOf(BazRendering::class to bazFactory)) val registry = CompositeViewRegistry(fooBarRegistry, bazRegistry) - assertThat(registry.buildView(FooRendering)).isSameInstanceAs(fooView) - assertThat(registry.buildView(BarRendering)).isSameInstanceAs(barView) - assertThat(registry.buildView(BazRendering)).isSameInstanceAs(bazView) + assertThat(registry.getFactoryFor(FooRendering::class)) + .isSameInstanceAs(fooFactory) + assertThat(registry.getFactoryFor(BarRendering::class)) + .isSameInstanceAs(barFactory) + assertThat(registry.getFactoryFor(BazRendering::class)) + .isSameInstanceAs(bazFactory) } - @Test fun `keys includes all composite registries' keys`() { - val fooBarRegistry = TestRegistry( - mapOf( - FooRendering::class to mock(), - BarRendering::class to mock() + @Test fun `getFactoryFor throws on missing registry`() { + val fooRegistry = TestRegistry(setOf(FooRendering::class)) + val registry = CompositeViewRegistry(fooRegistry) + + val error = assertFailsWith { + registry.getFactoryFor(BarRendering::class) + } + assertThat(error).hasMessageThat() + .isEqualTo( + "A ${ViewFactory::class.java.name} should have been registered to display a ${BarRendering::class}." ) - ) - val bazRegistry = TestRegistry(mapOf(BazRendering::class to mock())) + } + + @Test fun `keys includes all composite registries' keys`() { + val fooBarRegistry = TestRegistry(setOf(FooRendering::class, BarRendering::class)) + val bazRegistry = TestRegistry(setOf(BazRendering::class)) val registry = CompositeViewRegistry(fooBarRegistry, bazRegistry) assertThat(registry.keys).containsExactly( @@ -84,31 +81,18 @@ class CompositeViewRegistryTest { ) } - @Test fun `throws on missing binding`() { - val fooRegistry = TestRegistry(mapOf(FooRendering::class to mock())) - val registry = CompositeViewRegistry(fooRegistry) - - val error = assertFailsWith { - registry.buildView(BarRendering) - } - assertThat(error).hasMessageThat() - .isEqualTo( - "A ${ViewFactory::class.java.name} should have been registered to display $BarRendering." - ) - } - private object FooRendering private object BarRendering private object BazRendering - private class TestRegistry(private val bindings: Map, View>) : ViewRegistry { - override val keys: Set> get() = bindings.keys + private class TestRegistry(private val factories: Map, ViewFactory<*>>) : ViewRegistry { + constructor(keys: Set>) : this(keys.associateWith { TestViewFactory(it) }) + + override val keys: Set> get() = factories.keys - override fun buildView( - initialRendering: RenderingT, - initialViewEnvironment: ViewEnvironment, - contextForNewView: Context, - container: ViewGroup? - ): View = bindings.getValue(initialRendering::class) + @Suppress("UNCHECKED_CAST") + override fun getFactoryFor( + renderingType: KClass + ): ViewFactory = factories.getValue(renderingType) as ViewFactory } } diff --git a/kotlin/workflow-ui/core-android/src/test/java/com/squareup/workflow/ui/TestBinding.kt b/kotlin/workflow-ui/core-android/src/test/java/com/squareup/workflow/ui/TestViewFactory.kt similarity index 88% rename from kotlin/workflow-ui/core-android/src/test/java/com/squareup/workflow/ui/TestBinding.kt rename to kotlin/workflow-ui/core-android/src/test/java/com/squareup/workflow/ui/TestViewFactory.kt index eeddbd261..39ce80341 100644 --- a/kotlin/workflow-ui/core-android/src/test/java/com/squareup/workflow/ui/TestBinding.kt +++ b/kotlin/workflow-ui/core-android/src/test/java/com/squareup/workflow/ui/TestViewFactory.kt @@ -23,10 +23,9 @@ import kotlin.reflect.KClass import kotlin.test.fail fun ViewRegistry.buildView(rendering: R): View = - buildView(rendering, ViewEnvironment(this), mock()) + buildView(rendering, ViewEnvironment(this), mock()) -class TestBinding(override val type: KClass) : - ViewFactory { +class TestViewFactory(override val type: KClass) : ViewFactory { override fun buildView( initialRendering: R, initialViewEnvironment: ViewEnvironment, diff --git a/kotlin/workflow-ui/core-android/src/test/java/com/squareup/workflow/ui/ViewRegistryTest.kt b/kotlin/workflow-ui/core-android/src/test/java/com/squareup/workflow/ui/ViewRegistryTest.kt new file mode 100644 index 000000000..4c25b03e9 --- /dev/null +++ b/kotlin/workflow-ui/core-android/src/test/java/com/squareup/workflow/ui/ViewRegistryTest.kt @@ -0,0 +1,73 @@ +package com.squareup.workflow.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 + +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.workflow.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 + } +}