Skip to content

Commit

Permalink
Introduce a render binding function that works with AndroidX ViewBind…
Browse files Browse the repository at this point in the history
…ings.

Closes #984.
  • Loading branch information
zach-klippenstein committed Mar 7, 2020
1 parent 2552df0 commit 47d8764
Show file tree
Hide file tree
Showing 10 changed files with 141 additions and 22 deletions.
3 changes: 3 additions & 0 deletions kotlin/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ buildscript {
versionName "1.0"
}

buildFeatures.viewBinding = true

// See https://github.com/Kotlin/kotlinx.coroutines/issues/1064#issuecomment-479412940
packagingOptions {
exclude 'META-INF/atomicfu.kotlin_module'
Expand Down Expand Up @@ -67,6 +69,7 @@ buildscript {
// it doesn't really add value for us.
'savedstate': "androidx.savedstate:savedstate:1.0.0",
'transition': "androidx.transition:transition:1.3.1",
'viewbinding': "androidx.databinding:viewbinding:3.6.1",
],

'compose': [
Expand Down
2 changes: 2 additions & 0 deletions kotlin/gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
org.gradle.jvmargs='-Dfile.encoding=UTF-8'
org.gradle.parallel=true
android.useAndroidX=true
# Required for ViewBinding.
android.enableJetifier=true
# Uncomment this to diagnose "API $s is obsolete" warnings. Commented out b/c it's pretty noisy.
#android.debug.obsoleteApi=true

Expand Down
1 change: 1 addition & 0 deletions kotlin/samples/hello-workflow/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -35,5 +35,6 @@ dependencies {
implementation project(':workflow-runtime')

implementation deps.androidx.appcompat
implementation deps.androidx.viewbinding
implementation deps.rxjava2.rxjava2
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,25 +15,13 @@
*/
package com.squareup.sample.helloworkflow

import android.view.View
import android.widget.TextView
import com.squareup.sample.helloworkflow.HelloWorkflow.Rendering
import com.squareup.sample.helloworkflow.databinding.HelloGoodbyeLayoutBinding
import com.squareup.workflow.ui.LayoutRunner
import com.squareup.workflow.ui.LayoutRunner.Companion.bind
import com.squareup.workflow.ui.ViewFactory
import com.squareup.workflow.ui.ViewEnvironment

class HelloLayoutRunner(view: View) : LayoutRunner<HelloWorkflow.Rendering> {
private val messageView: TextView = view.findViewById(R.id.hello_message)

override fun showRendering(
rendering: HelloWorkflow.Rendering,
viewEnvironment: ViewEnvironment
) {
messageView.text = rendering.message
messageView.setOnClickListener { rendering.onClick() }
val HelloViewFactory: ViewFactory<Rendering> =
LayoutRunner.bind(HelloGoodbyeLayoutBinding::inflate) { rendering, _ ->
helloMessage.text = rendering.message
helloMessage.setOnClickListener { rendering.onClick() }
}

companion object : ViewFactory<HelloWorkflow.Rendering> by bind(
R.layout.hello_goodbye_layout, ::HelloLayoutRunner
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import com.squareup.workflow.ui.ViewRegistry
import com.squareup.workflow.ui.WorkflowRunner
import com.squareup.workflow.ui.setContentWorkflow

private val viewRegistry = ViewRegistry(HelloLayoutRunner)
private val viewRegistry = ViewRegistry(HelloViewFactory)

class HelloWorkflowActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:autoSizeTextType="uniform"
/>

</LinearLayout>
6 changes: 6 additions & 0 deletions kotlin/workflow-ui/core-android/api/core-android.api
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,12 @@ public final class com/squareup/workflow/ui/ShowRenderingTag {
public fun toString ()Ljava/lang/String;
}

public final class com/squareup/workflow/ui/ViewBindingViewFactory : com/squareup/workflow/ui/ViewFactory {
public fun <init> (Lkotlin/reflect/KClass;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function1;)V
public fun buildView (Ljava/lang/Object;Lcom/squareup/workflow/ui/ViewEnvironment;Landroid/content/Context;Landroid/view/ViewGroup;)Landroid/view/View;
public fun getType ()Lkotlin/reflect/KClass;
}

public final class com/squareup/workflow/ui/ViewEnvironment {
public fun <init> (Lcom/squareup/workflow/ui/ViewRegistry;)V
public fun equals (Ljava/lang/Object;)Z
Expand Down
2 changes: 2 additions & 0 deletions kotlin/workflow-ui/core-android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ apply from: rootProject.file('.buildscript/configure-dokka.gradle')
android rootProject.ext.defaultAndroidConfig

dependencies {
compileOnly deps.androidx.viewbinding

api project(':workflow-core')
api project(':workflow-ui:core-common')

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,12 @@ import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.annotation.LayoutRes
import androidx.viewbinding.ViewBinding
import com.squareup.workflow.ui.LayoutRunner.Companion.bind
import kotlin.reflect.KClass

typealias ViewBindingInflater<BindingT> = (LayoutInflater, ViewGroup?, Boolean) -> BindingT

/**
* A delegate that implements a [showRendering] method to be called when a workflow rendering
* of type [RenderingT] is ready to be displayed in a view inflated from a layout resource
Expand Down Expand Up @@ -51,6 +55,31 @@ import kotlin.reflect.KClass
* 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, ::HelloLayoutRunner
* )
* }
*
* If the view does not need to be initialized, the [bind] function can be used instead.
*/
interface LayoutRunner<RenderingT : Any> {
fun showRendering(
Expand All @@ -69,8 +98,7 @@ interface LayoutRunner<RenderingT : Any> {
contextForNewView: Context,
container: ViewGroup?
): View {
return LayoutInflater.from(container?.context ?: contextForNewView)
.cloneInContext(contextForNewView)
return contextForNewView.viewBindingLayoutInflater(container)
.inflate(layoutId, container, false)
.apply {
bindShowRendering(
Expand All @@ -92,6 +120,47 @@ interface LayoutRunner<RenderingT : Any> {
noinline constructor: (View) -> LayoutRunner<RenderingT>
): ViewFactory<RenderingT> = Binding(RenderingT::class, layoutId, constructor)

/**
* Creates a [ViewFactory] that [inflates][bindingInflater] a [ViewBinding] ([BindingT]) to show
* renderings of type [RenderingT], using [showRendering].
*
* ```
* val HelloBinding: ViewFactory<Rendering> =
* bindViewBinding(HelloGoodbyeLayoutBinding::inflate) { rendering, containerHints ->
* 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.
*/
inline fun <BindingT : ViewBinding, reified RenderingT : Any> bind(
noinline bindingInflater: ViewBindingInflater<BindingT>,
crossinline showRendering: BindingT.(RenderingT, ViewEnvironment) -> Unit
): ViewFactory<RenderingT> = bind(bindingInflater) { binding ->
object : LayoutRunner<RenderingT> {
override fun showRendering(
rendering: RenderingT,
viewEnvironment: ViewEnvironment
) = binding.showRendering(rendering, viewEnvironment)
}
}

/**
* Creates a [ViewFactory] that [inflates][bindingInflater] a [BindingT] to show renderings of
* type [RenderingT], using a [LayoutRunner] created by [constructor].
*
* 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.
*/
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],
* with a no-op [LayoutRunner]. Handy for showing static views.
Expand All @@ -108,3 +177,7 @@ interface LayoutRunner<RenderingT : Any> {
}
}
}

internal fun Context.viewBindingLayoutInflater(container: ViewGroup?) =
LayoutInflater.from(container?.context ?: this)
.cloneInContext(this)
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
/*
* Copyright 2020 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.workflow.ui

import android.content.Context
import android.view.View
import android.view.ViewGroup
import androidx.viewbinding.ViewBinding
import kotlin.reflect.KClass

@PublishedApi
internal class ViewBindingViewFactory<BindingT : ViewBinding, RenderingT : Any>(
override val type: KClass<RenderingT>,
private val bindingInflater: ViewBindingInflater<BindingT>,
private val runnerConstructor: (BindingT) -> LayoutRunner<RenderingT>
) : ViewFactory<RenderingT> {
override fun buildView(
initialRendering: RenderingT,
initialViewEnvironment: ViewEnvironment,
contextForNewView: Context,
container: ViewGroup?
): View =
bindingInflater(contextForNewView.viewBindingLayoutInflater(container), container, false)
.also { binding ->
binding.root.bindShowRendering(
initialRendering,
initialViewEnvironment,
runnerConstructor.invoke(binding)::showRendering
)
}
.root
}

0 comments on commit 47d8764

Please sign in to comment.