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.