Android databinding, with Kotlin!

15 January 2018

The MVVM design pattern allows us to write code that is much easier to maintain, test, and reason about in Android development. In this post, we will describe how to setup an application for databinding using the `MVVM` pattern in Kotlin, although many of the concepts are also equally valid for `MVP`

Getting Started

This post walks through the steps to set up Android databinding using the MVVM pattern in Kotlin. If you learn best by playing directly with code, I have provided a simple sample app that that implements all of the features discussed below.

First Steps

First we require a few prerequisites for our Android project:

Add the Kotlin plugin to your gradle file:

apply plugin: "kotlin-android"
apply plugin: "kotlin-android-extensions"
apply plugin: "kotlin-kapt"

Enable databinding in the gradle file:

android {
   dataBinding {
       enabled = true
   }
}

Add databinding as a kapt dependency

dependencies {
    ...
    kapt "com.android.databinding:compiler:$androidPluginVersion"
    ...
}

Add the Kotlin standard library as a dependency

dependencies {
    ...
    implementation "org.jetbrains.kotlin:kotlin-stdlib-jre7:$kotlinVersion"
    ...
}

What is Android data binding, and why should I use it?

Android data binding allows for 1 or 2-way binding within the layout XML files of Android. This should be familiar to anyone who has done web based development with Angular or Ember, or C# WPF development.

The most basic case is to add an on-click listener. This requires no registering of on click listeners in any of your application code, just some XML in the layout file and a function to be called in your ViewModel class

<layout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto">
    <data>
        <import type="android.view.View"/>
        <variable
            name="vm"
            type="co.instil.databinding.DemoViewModel"/>
    </data>
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical">

        <Button
            android:layout_width="fill_parent"
            android:layout_height="200dp"
            android:text="Click Me!"
            android:visibility="@{vm.buttonVisible ? View.VISIBLE : View.GONE}"
            android:onClick="@{() -> vm.buttonClicked()}"/>
        <TextView
            android:layout_width="fill_parent"
            android:layout_height="200dp"
            android:text="@{vm.text}"/>
    </LinearLayout>
</layout>

If you are new to databinding the above might look a bit odd - why is my root element <layout>, what is this <data> tag, what is that crazy string in the onClick attribute? Do not worry if it is confusing now, we will walk you through it line by line.

First the layout element:

<layout>

All Android layout files that you wish to enable databinding for must start with this tag. This tag will only contain namespace declarations, not any height or width specifications. It will then include any layout or view within it as a child element, just as you would any other layout file.

xmlns:app="http://schemas.android.com/apk/res-auto"

This namespace gives access to the app attributes. These are custom attributes provided by either your project (more on this later!), or by the Android library itself. An example will be shown later of this in action.

<data>

The data tag is used to include all your Java / Kotlin values you wish to inject into the layout file.

<import type="android.view.View"/>

The import tag is used to import any type that is used within the layout. Later we will show you an example utilising this concept to determine the visibility of the view.

<variable
            name="vm"
            type="co.instil.demo.DemoViewModel"/>

The variable tag is used to store any values that will be used in the layout. In this case the DemoViewModel class is loaded as it contains the business logic which will control this view.

android:visibility="@{vm.buttonVisible ? View.VISIBLE : View.GONE}"

Here we are accessing the boolean field on the DemoViewModel called buttonVisible, and if it is true, then we are setting the visibility of the Button to View.VISIBLE, otherwise we are setting it to View.GONE. These values from the View class require an import as described above.

android:onClick="@{() -> vm.buttonClicked()}"

Here we can register a lambda which, when the user clicks on the button, will execute the provided code. In our case it will just call the buttonClicked method on the DemoViewModel class.

android:text="@{vm.text}"

Finally we are binding the standard android attribute android:text to the string value contained in the field text in the DemoViewModel class. The syntax looks a bit weird but basically @ is used to tell the auto generated data binding class to replace this value, with whatever is inside the vm.text field. For 2 way binding (e.g. changes on a text view update the value in the view model) then the sytax would be @=, e.g. @={myValue}.

Setting up a view for Databinding

In order to actually use databinding we need to set the content view of our activity using the Android provided android.databinding.DataBindingUtil class. This will return an auto generated ViewDataBinding class specific to our layout XML file, which allows us to inject any variables we need. In this case we need to inject the DemoViewModel class into the layout.

class DemoActivity : AppCompatActivity() {
     override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val binding = DataBindingUtil.setContentView<ActivityDemoBinding>(this, R.layout.activity_demo)
        binding.vm = DemoViewModel() // Injecting the view model into the layout file
    }
}
class DemoViewModel {
    val buttonVisible = true
    val text = ObservableField("Data binding works!")
    fun buttonClicked() {
        text.set("Button clicked!")
    }
}

As you can see with the above code there is no need to directly access any View elements in the code, and all business logic is handled within the ViewModel, with the minimum just to setup the view inside the Activity.

The only odd thing in the above code is the use of ObservableField. An ObservableField is a simple data wrapper which will allow the layout to be notified whenever the value has changed. After using the set method in the buttonClicked function, it will update the internal String with the new value, and then notify any listeners (in our case the layout) of the change. The ObservableField can be used for any type as it allows generics e.g. ObservableField<Boolean>, ObservableField<Int>.

Creating your own custom attributes

So the MVVM architecture looks great right? What about if we want to change the text color of the TextView dynamically based on character length? Does that mean we have to reference the View within our ViewModel (breaking the idea that no Android specific code should be in the ViewModel)? We don’t have to! There are two ways to create your own custom attributes for a view.

First we will create some helpful extension methods to make detecting text changes easier:

fun EditText.onTextChanged(action: (CharSequence) -> Unit) {
    addTextChangedListener(object : TextWatcher {
        override fun afterTextChanged(string: Editable?) = Unit
        override fun beforeTextChanged(string: CharSequence?, start: Int, count: Int, after: Int) = Unit
        override fun onTextChanged(string: CharSequence?, start: Int, before: Int, count: Int) {
            action(string ?: "")
        }
    })
}

fun EditText.clearOnTextChangedListener() {
    onTextChanged {}
}

Then, using a @BindingAdapter("myCustomAttribute") annotation you can specify package function as a handler for a view's attribute:

@BindingAdapter("textLengthWarning")
fun textLengthWarning(view: EditText, textLengthWarningEnabled: Boolean) {
    if (!textLengthWarningEnabled) {
        view.clearOnTextChangedListener()
        return
    }

    view.onTextChanged {
        if (view.text.length > 10) {
            view.setTextColor(view.context.getColor(R.color.red))
        } else {
            view.setTextColor(view.context.getColor(R.color.black))
        }
    }
}

Or an alternative way is to create a custom view which will contain your business logic:

class EditTextWithTextLimitCheck @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0) : EditText(context, attrs, defStyleAttr) {
    fun setTextLengthWarning(enabled: Boolean) {
        if (enabled) {
            enableTextLimitCheck()
        } else {
            disableTextLimitCheck()
        }
    }

    private fun enableTextLimitCheck() {
        onTextChanged { text ->
            if (text.length > 10) {
                setTextColor(context.getColor(R.color.red))
            } else {
                setTextColor(context.getColor(R.color.black))
            }
        }
    }

    private fun disableTextLimitCheck() {
        clearOnTextChangedListener()
    }
}

Then in the layout, regardless of which of the above approaches are used, you bind some data to your new attribute:

<EditText
    ...
    app:textLengthWarning="@{true}"/>

<EditTextWithTextLimitCheck
    ...
    app:textLengthWarning="@{true}"/>

How does databinding work?

In order for the layout to interact with these injected values some glue code is generated by Android. This glue code will handle the on click listeners, binding adapters, and 2 way binding for us. So as developers we can just focus on the actual business logic.

Sample app

You can find the sample app that implements all of the features discussed above here: https://github.com/instil/kotlin-databinding

Wrapping up

Here at Instil we have been very much swept off our feet by the power and readability offered by Kotlin. The best bit being you don’t have to stop using any of the best practices you already know from Java, as Kotlin has such strong interoperability with Java while also being backed up by the fantastic tooling provided by Jetbrains in both IntelliJ and Android Studio.

Article By
blog author

Neil Armstrong

Senior Engineer