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 6, 2020
1 parent 864ad23 commit c15e266
Show file tree
Hide file tree
Showing 9 changed files with 121 additions and 22 deletions.
1 change: 1 addition & 0 deletions kotlin/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,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
3 changes: 3 additions & 0 deletions kotlin/samples/hello-workflow/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ android {

testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}

buildFeatures.viewBinding = true
}

dependencies {
Expand All @@ -36,6 +38,7 @@ dependencies {
implementation project(':workflow-runtime')

implementation deps.androidx.appcompat
implementation deps.androidx.viewbinding
implementation deps.rxjava2.rxjava2

androidTestImplementation deps.test.androidx.espresso.core
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,25 +15,14 @@
*/
package com.squareup.sample.helloworkflow

import android.view.View
import android.widget.TextView
import com.squareup.workflow.ui.ContainerHints
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.RenderBinding
import com.squareup.workflow.ui.bind

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

override fun showRendering(
rendering: HelloWorkflow.Rendering,
containerHints: ContainerHints
) {
messageView.text = rendering.message
messageView.setOnClickListener { rendering.onClick() }
val HelloBinding: RenderBinding<Rendering> =
LayoutRunner.bind(HelloGoodbyeLayoutBinding::inflate) { rendering, _ ->
helloMessage.text = rendering.message
helloMessage.setOnClickListener { rendering.onClick() }
}

companion object : RenderBinding<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(HelloBinding)

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>
4 changes: 4 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,10 @@ apply from: rootProject.file('.buildscript/configure-dokka.gradle')
android rootProject.ext.defaultAndroidConfig

dependencies {
// ViewBinding is compileOnly because we don't want to include it as a transitive dependency
// if consumers aren't actually using view binding.
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
@@ -0,0 +1,34 @@
package com.squareup.workflow.ui

import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.viewbinding.ViewBinding

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

/**
* Creates a [RenderBinding] that [inflates][bindingInflater] a [ViewBinding] ([BindingT]) to show
* renderings of type [RenderingT], using [showRendering].
*
* ```
* val HelloBinding: RenderBinding<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> LayoutRunner.Companion.bind(
noinline bindingInflater: ViewBindingInflater<BindingT>,
crossinline showRendering: BindingT.(RenderingT, ContainerHints) -> Unit
): RenderBinding<RenderingT> = bind(bindingInflater) { binding ->
object : LayoutRunner<RenderingT> {
override fun showRendering(
rendering: RenderingT,
containerHints: ContainerHints
) = binding.showRendering(rendering, containerHints)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ 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

/**
Expand Down Expand Up @@ -51,6 +53,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 : RenderBinding<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 +96,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 +118,20 @@ interface LayoutRunner<RenderingT : Any> {
noinline constructor: (View) -> LayoutRunner<RenderingT>
): RenderBinding<RenderingT> = Binding(RenderingT::class, layoutId, constructor)

/**
* Creates a [RenderBinding] 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>
): RenderBinding<RenderingT> =
AndroidXRenderBinding(RenderingT::class, bindingInflater, constructor)

/**
* Creates a [RenderBinding] that inflates [layoutId] to "show" renderings of type [RenderingT],
* with a no-op [LayoutRunner]. Handy for showing static views.
Expand All @@ -106,5 +146,32 @@ interface LayoutRunner<RenderingT : Any> {
) = Unit
}
}

private fun Context.viewBindingLayoutInflater(container: ViewGroup?) =
LayoutInflater.from(container?.context ?: this)
.cloneInContext(this)

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

0 comments on commit c15e266

Please sign in to comment.