Implement Data Binding with Kotlin
Android launch Data Binding library for several years. The Data Binding Library offers both flexibility and broad compatibility. You can use it with devices running Android 4.0 (API level 14) or higher. It’s an option to apply since we still can use the local way to write our code. So what’s the benefit to apply it to our project? I would say. It will let our code clean. Let’s see how we implement it.
In build.gradle
we can easily enable it by a toggle.
apply plugin: 'kotlin-kapt'android {
... //skip above your config
dataBinding {
enabled = true
}
}
Then no matter what architecture pattern you using (MVC,MVP, or MVVM)
You will have a model like this User.kt
data class User(
var name: String?,
var id: Int,
var avatarUrl: String?,
var company: String?
)
Then in your xml file. Please wrap your view with <Layout></Layout>
tag.
And add <data></data>
this means you will binding the views and data model together in this activity_user.xml
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
<variable
name="user"
type="your.package.name.User"/>
</data><android.support.constraint.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@android:color/white">
<ImageView android:id="@+id/avatar"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintWidth_percent="0.4"
app:layout_constraintDimensionRatio="W, 1:1"
android:scaleType="centerCrop"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:imageUrl="@{user.avatarUrl}"/>
<TextView android:id="@+id/nameTextView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
app:layout_constraintTop_toBottomOf="@id/avatar"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:text="@{user.company}"
tools:text="Chris Wanstrath"/> <!-- ...skip layouts --></android.support.constraint.ConstraintLayout></layout>
Then we need build project to generate some component.
In UserActivity we claim to bind as lateinit var
because it must be inited in onCreate
and never be null.
class UserActivity : Activity() {
private lateinit var binding: ActivityUserBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) // setContentView(R.layout.activity_user) remove this line for now
binding = DataBindingUtil.setContentView(this, R.layout.activity_user)
val user = User("Chris", 0, "url", "google")
binding.user = user
}
}
That’s it. The view and data binding together. If the data value changed. the view will automatically change then.
Also support primitive type and String for data in xml.
<data>
<variable
name="myBoolean"
type="boolean" />
<variable
name="myInt"
type="int" /> <variable
name="myLong"
type="long" /> <variable
name="myFloat"
type="float" /> <variable
name="myDouble"
type="double" /> <variable
name="myString"
type="String" /></data>
That’s the basic usage for data binding. Then you might interest that what about load image? This looks like doesn’t work at all. It’s true. Data binding provide some further usage method. I will introduce how to use BindingAdapter
.
With ViewModel
If you want to bindLivaData
in view model. Remember to add lifecycleOwner
. Let’s see the comment from google:
Sets the {@link LifecycleOwner} that should be used for observing changes of
LiveData in this binding. If a {@link LiveData} is in one of the binding expressions and no LifecycleOwner is set, the LiveData will not be observed and updates to it will not be propagated to the UI.
class UserActivity : Activity() {
private lateinit var binding: ActivityUserBinding private lateinit var viewModel: MainViewModeloverride fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = DataBindingUtil.setContentView(this, R.layout.activity_user) viewModel = MainViewModel()
binding.lifecycleOwner = this
binding.vm = viewModel
}
}
BindingAdapter
Here is our requirement. I want a circle avatar. First of all, we create a singleton class UiUtil
and implement loadImageInCircle
method. It’s implemented by Glide
so maybe you need to add dependance in your build.gradle
file.
object UiUtil {
@BindingAdapter(value = ["imageUrl", "placeholder"], requireAll = false)
@JvmStatic
fun loadImageInCircle(view: ImageView, imageUrl: String?, placeholder: Drawable? = null) {
Glide.with(view.context)
.load(imageUrl)
.apply(RequestOptions.circleCropTransform())
.error(placeholder)
.into(view)
}
}
The value
means in the xml, we need to define both imageUrl
and placeholder
. The requireAll
means we can just define one of them rather than all. After rebuild, we can use app:imageUrl
in xml now. Finally will look like
<ImageView android:id="@+id/avatar"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scaleType="centerCrop"
app:imageUrl="@{user.avatarUrl}"/>
We don’t need to call loadImageInCircle
in our code. Just add one line then we can support circle crop image. :)
Logic in xml
We can also write some logic in xml. Like below isShow
is a boolean type to determine the nameTextView
show or hide. And we need to import android.view.View
in xml data block.
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
<import type="android.view.View"/>
<variable
name="user"
type="your.package.name.User"/> <variable
name="isShow"
type="boolean"/>
</data><!--- skip layout ...-><TextView android:id="@+id/nameTextView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="@{isShow ? View.GONE: View.VISIBLE}"
android:text="@{user.name}"
tools:text="Chris Wanstrath"/><!--- skip layout ...->
</layout>
Although we can use this way. But in my personal opinion. I would encourage you to put this kind of logic in Presenter
or ViewModel
. Because you can write Test to protect your business logic.
BindingConversion
In some case, a custom conversion is required between specific types. For example, the android:background
expects a Drawable
, but the color
value specified is an integer. So we can define BindingConversion here.
@BindingConversion
fun convertColorToDrawable(color: Int) = ColorDrawable(color)
Then android:background
support color now. we can define in our xml.
<View
android:background="@{isError ? @color/red : @color/white}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
Conclusion
Let’s wrap up. Here are some pros and cons of data binding.
Pros:
- Reduces boilerplate code to make the code base clean.
- Easy implement custom attrs and custom views.
- The library extracts the views including the IDs from the view hierarchy in a single pass. This mechanism can be faster than calling the findViewById() method for every view in the layout. (reference)
Cons:
- If your data model is nested. you might need to check the null case. Hence, better to create a method in your parent cover the null check.
- Auto generates class causes the app size to increase.
- UI isn’t testable if you write business in xml. You should avoid doing that.
Github example: here