Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -98,9 +97,8 @@ class OverviewDetailContainer(view: View) : LayoutRunner<OverviewDetailScreen> {
stub.update(combined, viewEnvironment + (OverviewDetailConfig to Single))
}

companion object : ViewFactory<OverviewDetailScreen> by LayoutRunnerViewFactory(
type = OverviewDetailScreen::class,
companion object : ViewFactory<OverviewDetailScreen> by LayoutRunner.bind(
layoutId = R.layout.overview_detail,
runnerConstructor = ::OverviewDetailContainer
constructor = ::OverviewDetailContainer
)
}
5 changes: 0 additions & 5 deletions workflow-ui/core-android/api/core-android.api
Original file line number Diff line number Diff line change
Expand Up @@ -105,18 +105,13 @@ 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 {
public fun getDefault ()Lcom/squareup/workflow1/ui/ViewRegistry;
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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,9 @@ internal class CompositeViewRegistry private constructor(

override val keys: Set<KClass<*>> get() = registriesByKey.keys

override fun <RenderingT : Any> getFactoryFor(
override fun <RenderingT : Any> getViewFactoryFor(
renderingType: KClass<out RenderingT>
): ViewFactory<RenderingT> = getRegistryFor(renderingType).getFactoryFor(renderingType)
): ViewFactory<RenderingT> = getRegistryFor(renderingType).getViewFactoryFor(renderingType)

private fun getRegistryFor(renderingType: KClass<out Any>): ViewRegistry {
return requireNotNull(registriesByKey[renderingType]) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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>(NeutronFlowPolarity::class) {
* override val default: NeutronFlowPolarity = NeutronFlowPolarity(reversed = false)
* }
Expand Down Expand Up @@ -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.
*/
Expand All @@ -101,7 +101,7 @@ class DecorativeViewFactory<OuterT : Any, InnerT : Any>(
private val initView: (OuterT, View) -> Unit = { _, _ -> },
private val doShowRendering: (
view: View,
innerShowRendering: ViewShowRendering<InnerT>,
innerShowRendering: ShowRendering<InnerT>,
outerRendering: OuterT,
env: ViewEnvironment
) -> Unit = { _, innerShowRendering, outerRendering, viewEnvironment ->
Expand All @@ -118,7 +118,7 @@ class DecorativeViewFactory<OuterT : Any, InnerT : Any>(
initView: (OuterT, View) -> Unit = { _, _ -> },
doShowRendering: (
view: View,
innerShowRendering: ViewShowRendering<InnerT>,
innerShowRendering: ShowRendering<InnerT>,
outerRendering: OuterT,
env: ViewEnvironment
) -> Unit = { _, innerShowRendering, outerRendering, viewEnvironment ->
Expand Down Expand Up @@ -147,7 +147,7 @@ class DecorativeViewFactory<OuterT : Any, InnerT : Any>(
container
)
.also { view ->
val innerShowRendering: ViewShowRendering<InnerT> = view.getShowRendering()!!
val innerShowRendering: ShowRendering<InnerT> = view.getShowRendering()!!
initView(initialRendering, view)
view.bindShowRendering(initialRendering, processedInitialEnv) { rendering, env ->
doShowRendering(view, innerShowRendering, rendering, env)
Expand Down
Original file line number Diff line number Diff line change
@@ -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<RenderingT : ModalRendering> : ViewRegistry.Entry<RenderingT> {
/**
* 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
}
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -32,54 +17,8 @@ typealias ViewBindingInflater<BindingT> = (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<Rendering> {
* 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<Rendering> 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<Rendering> {
*
* override fun showRendering(rendering: Rendering) {
* binding.messageView.text = rendering.message
* binding.messageView.setOnClickListener { rendering.onClick(Unit) }
* }
*
* companion object : ViewFactory<Rendering> 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<RenderingT : Any> {
Expand All @@ -90,28 +29,18 @@ interface LayoutRunner<RenderingT : Any> {

companion object {
/**
* Creates a [ViewFactory] that inflates [layoutId] to show renderings of type [RenderingT],
* using a [LayoutRunner] created by [constructor].
*/
inline fun <reified RenderingT : Any> bind(
@LayoutRes layoutId: Int,
noinline constructor: (View) -> LayoutRunner<RenderingT>
): ViewFactory<RenderingT> = 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<Rendering> =
* bindViewBinding(HelloGoodbyeLayoutBinding::inflate) { rendering, viewEnvironment ->
* helloMessage.text = rendering.message
* helloMessage.setOnClickListener { rendering.onClick(Unit) }
* }
* ```
* val HelloBinding: ViewFactory<Rendering> =
* 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 <BindingT : ViewBinding, reified RenderingT : Any> bind(
noinline bindingInflater: ViewBindingInflater<BindingT>,
Expand All @@ -126,22 +55,47 @@ interface LayoutRunner<RenderingT : Any> {
}

/**
* 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<Rendering> {
*
* override fun showRendering(rendering: Rendering) {
* binding.messageView.text = rendering.message
* binding.messageView.setOnClickListener { rendering.onClick(Unit) }
* }
*
* companion object : ViewFactory<Rendering> 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 <BindingT : ViewBinding, reified RenderingT : Any> bind(
noinline bindingInflater: ViewBindingInflater<BindingT>,
noinline constructor: (BindingT) -> LayoutRunner<RenderingT>
): ViewFactory<RenderingT> =
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 <reified RenderingT : Any> bind(
@LayoutRes layoutId: Int,
noinline constructor: (View) -> LayoutRunner<RenderingT>
): ViewFactory<RenderingT> = 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. e.g. when prototyping.
*/
inline fun <reified RenderingT : Any> bindNoRunner(
@LayoutRes layoutId: Int
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,12 @@ 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
class LayoutRunnerViewFactory<RenderingT : Any>(
@PublishedApi
internal class LayoutRunnerViewFactory<RenderingT : Any>(
override val type: KClass<RenderingT>,
@LayoutRes private val layoutId: Int,
private val runnerConstructor: (View) -> LayoutRunner<RenderingT>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package com.squareup.workflow1.ui

import android.app.Dialog
import android.content.Context

@WorkflowUiExperimentalApi
interface ModalRendering

@WorkflowUiExperimentalApi
fun <RenderingT: ModalRendering> RenderingT.buildDialog(
initialViewEnvironment: ViewEnvironment,
context: Context
): Dialog {
val dialogFactory = initialViewEnvironment[ViewRegistry].getEntryFor(this::class)
require(dialogFactory is DialogFactory<RenderingT>) {
"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<Any>() != null) {
"Dialog.bindShowRendering should have been called for $this, typically by the " +
"${DialogFactory::class.java.name} that created it."
}
}
}
Loading