This tutorial is part of my Android Jetpack tutorials and source can be found here

In this tutorial, we are going to make an Android app that uses the Data Binding library but first…

What is Data Binding in Android?

Data Binding is binding data to UI components. Instead of just “setting” data to a UI component, “binding” data to a UI component is “setting” the data AND “coupling” it with the UI component so that if the data changes, the UI component(s) that are bind to that data are also affected.

Structure Of An Android Layout With Data Binding

Root tag is <layout> Imports and Variables are at the top Wrap import and variable tags with a <data> tag Data Binding usage at the bottom

It’s structured like a normal class where our imports and most of our variables are declared at the top and we use them at the bottom.

Tags

<layout> – tells the compiler that this layout has data binding in it so generate the binding class for this layout.

<import> – similar to how we interpret imports in normal classes. Import a class in this layout.

<variable> – similar to how we interpret variables in normal classes. Declare a variable and it’s type for the layout to use.

<data> – wraps <import> and <variable> tags for the “data” that we are going bind for this layout.

Binding Data

When you set the root tag of your layout to <layout>, a binding class is generated by the Data Binding library. For example, activity_main.xml will have a binding class called ActivityMainBinding.

This class holds all the bindings from the layout properties to the layout’s views and knows how to assign values for the binding expressions.

The recommended method to create the bindings is to do it while inflating the layout:

override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) val binding: ActivityMainBinding = DataBindingUtil.setContentView(this, R.layout.activity_main) val user = User("Test", "User") binding.user = user }

If you are using data binding items in a Fragment, ListView, or RecyclerView adapter, use the inflate() methods of the binding classes or the DataBindingUtil class:

val binding = ListItemBinding.inflate(layoutInflater, viewGroup, false) // or val binding = DataBindingUtil.inflate(layoutInflater, R.layout.list_item, viewGroup, false)

What are we going to make?

In this tutorial, we will make a very simple app that demonstrates how we can use data binding in Android. The theme for this app is inspired by a game that is very close to my heart – Dota 2.

We will display a pre-determined list of heroes and when you click a hero it shows some details (name, abilities, etc.)

1. Setting up our project for Data Binding

1. Create a new project

2. Open your app/build.gradle file and enable data binding

3. Add highlighted dependencies

4. Sync gradle

apply plugin: 'com.android.application' apply plugin: 'kotlin-android' apply plugin: 'kotlin-android-extensions' apply plugin: 'kotlin-kapt' android { ... dataBinding { enabled = true } defaultConfig { ... } buildTypes { ... } } dependencies { ... implementation 'com.android.support:recyclerview-v7:27.1.1' implementation 'com.google.code.gson:gson:2.8.5' }

2. Create heroes.json

1. Right click app/src/main folder

2. New -> Directory

3. Name it assets

4. Right click assets folder

5. New -> File

6. Name it heroes.json

7. Paste this in your heroes.json file or you can grab your top ten most played heroes at DotaBuff if you have an account.

[ { "id": "1", "name": "Chen", "matches": 66, "abilities": "Penitence, Test of Faith, Holy Persuasion, Hand of God" }, { "id": "2", "name": "Windranger", "matches": 62, "abilities": "Shackleshot, Powershot, Windrun, Focus Fire" }, { "id": "3", "name": "Rubick", "matches": 55, "abilities": "Telekinesis, Fade Bolt, Null Field, Spell Steal" }, { "id": "4", "name": "Visage", "matches": 27, "abilities": "Grave Chill, Soul Assumption, Gravekeeper's Cloak, Stone Form, Summon Familiars" }, { "id": "5", "name": "Sand King", "matches": 26, "abilities": "Burrowstrike, Sand Storm, Caustic Finale, Epicenter" }, { "id": "6", "name": "Earthshaker", "matches": 23, "abilities": "Fissure, Enchant Totem, Aftershock, Echo Slam" }, { "id": "7", "name": "Earth Spirit", "matches": 17, "abilities": "Boulder Smash, Rolling Boulder, Geomagnetic Grip, Stone Remnant, Enchant Remnant, Magnetize" }, { "id": "8", "name": "Lina", "matches": 15, "abilities": "Dragon Slave, Light Strike Array, Fiery Soul, Laguna Blade" }, { "id": "9", "name": "Nyx Assassin", "matches": 14, "abilities": "Impale, Mana Burn, Spiked Carapace, Burrow, Vendetta, Unburrow" }, { "id": "10", "name": "Shadow Demon", "matches": 14, "abilities": "Disruption, Soul Catcher, Shadow Poison, Shadow Poison Release, Demonic Purge" } ]

3. Loading our heroes.json file

1. Create a new class called Hero

data class Hero(val id: String, val name: String, var matches: Int, val abilities: String)

2. Create a new class called HeroesRepository

class HeroesRepository(val heroes: List<Hero>) { companion object { private val TAG = HeroesRepository::class.java.simpleName @Volatile private var INSTANCE: HeroesRepository? = null private var heroes: List<Hero> = mutableListOf() fun getInstance(context: Context): HeroesRepository = INSTANCE ?: synchronized(this) { INSTANCE ?: buildRepository(context).also { INSTANCE = it } } private fun buildRepository(context: Context): HeroesRepository { val type = object : TypeToken<List<Hero>>() {}.type var jsonReader: JsonReader? = null try { val inputStream = context.assets.open(HEROES_FILENAME) jsonReader = JsonReader(inputStream.reader()) heroes = Gson().fromJson(jsonReader, type) } catch (e: Exception) { Log.e(TAG, "Error reading json file", e) } finally { jsonReader?.close() } return HeroesRepository(heroes) } } fun fetchHeroes(): List<Hero> { return heroes } fun getHero(heroId: String): Hero? { val heroes = fetchHeroes() for (hero in heroes) { if (hero.id.contentEquals(heroId)) { return hero } } return null } }

We’re creating a singleton class for us to read the heroes inside our heroes.json file. If you don’t know how to create a singleton with arguments in Kotlin you can read more about it here.

You can also checkout Florina Muntenescu’s gist for pre-populating a room database where she demonstrated how to create a singleton Room database.

It’s time to get our hands dirty on data binding!

4. Access data in your layout through Layout Expressions

1. Create a new layout – item_hero.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"> <data> <variable name="hero" type="com.imakeanapp.androiddatabindingsample.Hero" /> </data> <android.support.constraint.ConstraintLayout android:layout_width="match_parent" android:layout_height="wrap_content"> <TextView android:id="@+id/hero_name" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_marginStart="8dp" android:layout_marginTop="8dp" android:text="@{hero.name}" android:textAppearance="@style/TextAppearance.AppCompat.Medium" android:textStyle="bold" app:layout_constraintEnd_toStartOf="@+id/hero_matches" app:layout_constraintHorizontal_bias="0.5" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> <TextView android:id="@+id/hero_matches" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginEnd="8dp" android:layout_marginStart="8dp" android:text="@{String.valueOf(hero.matches)}" android:textAlignment="center" app:layout_constraintBottom_toBottomOf="@+id/hero_name" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintHorizontal_bias="0.5" app:layout_constraintStart_toEndOf="@+id/hero_name" app:layout_constraintTop_toTopOf="@+id/hero_name" /> <TextView android:id="@+id/hero_abilities" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_marginBottom="8dp" android:layout_marginEnd="8dp" android:layout_marginStart="8dp" android:layout_marginTop="8dp" android:text="@{hero.abilities}" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/hero_name" /> </android.support.constraint.ConstraintLayout> </layout>

Whenever you add <layout> to an xml layout, the Data Binding library automatically generates a binding class that you can use to bind the data. If somehow the binding class is not generated, go to Build -> Make Project.

2. Create a new class called HeroesAdapter

class HeroesAdapter( private val heroes: List<Hero> ) : RecyclerView.Adapter<HeroesAdapter.HeroViewHolder>() { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): HeroViewHolder { return HeroViewHolder( ItemHeroBinding.inflate( LayoutInflater.from(parent.context), parent, false ) ) } override fun getItemCount(): Int { return heroes.size } override fun onBindViewHolder(holder: HeroViewHolder, position: Int) { holder.bind(heroes[position], callback) } class HeroViewHolder( private val binding: ItemHeroBinding ) : RecyclerView.ViewHolder(binding.root) { fun bind(item: Hero) { binding.apply { hero = item } } } }

3. Open your activity_main.xml

<?xml version="1.0" encoding="utf-8"?> <android.support.constraint.ConstraintLayout 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" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".MainActivity"> <android.support.v7.widget.RecyclerView android:id="@+id/heroes_list" android:layout_width="match_parent" android:layout_height="match_parent" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> </android.support.constraint.ConstraintLayout>

4. Open your MainActivity.class

class MainActivity : AppCompatActivity() { private lateinit var recyclerView: RecyclerView override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) recyclerView = findViewById(R.id.heroes_list) recyclerView.layoutManager = LinearLayoutManager(this) val repository = HeroesRepository.getInstance(applicationContext) val heroes = repository.fetchHeroes() val adapter = HeroesAdapter(heroes) recyclerView.adapter = adapter } }

5. Run the app and you should now see the list 🎉

5. Handle events through Method References or Listener Bindings

There are two ways to handle events in Data Binding:

Method References

Method References are similar to how you assign a method in your activity to the android:onClick attribute. Difference is that it’s processed at compile time so if the method doesn’t exist or its signature is incorrect, you get a compile time error.

interface OnUserClickListener { fun onUserClick(view: View) }

Take a look at the parameter. The method that you assign to android:onClick attribute must have a View parameter because we should follow the signature of View.OnClickListener. To use it:

<layout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto"> <data> <variable name="listener" type="com.imakeanapp.androiddatabindingsample.OnUserClickListener"/> </data> <android.support.constraint.ConstraintLayout android:layout_width="match_parent" android:layout_height="match_parent" android:onClick="@{listener::onUserClick}"> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Hello World!" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintRight_toRightOf="parent" app:layout_constraintTop_toTopOf="parent" /> </android.support.constraint.ConstraintLayout> </layout>

What if you don’t want a View parameter? Let’s say when a view is clicked you want to receive a different parameter.

Listener Bindings

Listener Bindings are similar to method references. The key difference is that listener bindings allow you to pass arbitrary data (e.g. User, String, int, etc.).

interface OnUserClickListener { fun onUserClick(user: User) }

Now, we can pass a different parameter. To use it:

<?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"> <data> <variable name="user" type="com.imakeanapp.androiddatabindingsample.User"/> <variable name="listener" type="com.imakeanapp.androiddatabindingsample.OnUserClickListener"/> </data> <android.support.constraint.ConstraintLayout android:layout_width="match_parent" android:layout_height="match_parent" android:onClick="@{() -> listener.onUserClick(user)}"> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Hello World!" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintRight_toRightOf="parent" app:layout_constraintTop_toTopOf="parent" /> </android.support.constraint.ConstraintLayout> </layout>

Take a look at this part:

android:onClick="@{() -> listener.onUserClick(user)}

Important thing to note is that () is a View.OnClickListener that will be created when the view is clicked and then executes listener.onUserClick(user). Let’s say you want to use the View and pass the User object, you can do this:

android:onClick="@{(view) -> listener.onUserClick(view, user)}

For our app, we will use Listener Bindings to pass a Hero object:

1. Open your HeroesAdapter.class

class HeroesAdapter( private val heroes: List<Hero>, private val callback: OnHeroClickListener ) : RecyclerView.Adapter<HeroesAdapter.HeroViewHolder>() { interface OnHeroClickListener { fun onHeroClick(hero: Hero) } override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): HeroViewHolder { ... } ... class HeroViewHolder( private val binding: ItemHeroBinding ) : RecyclerView.ViewHolder(binding.root) { fun bind(item: Hero, callback: OnHeroClickListener) { binding.apply { hero = item listener = callback } } } }

2. Open item_hero.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"> <data> <variable name="hero" type="com.imakeanapp.androiddatabindingsample.Hero" /> <variable name="listener" type="com.imakeanapp.androiddatabindingsample.HeroesAdapter.OnHeroClickListener"/> </data> <android.support.constraint.ConstraintLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:onClick="@{() -> listener.onHeroClick(hero)}"> ... </android.support.constraint.ConstraintLayout> </layout>

3. Open your MainActivity.class and let’s fix the errors

class MainActivity : AppCompatActivity(), HeroesAdapter.OnHeroClickListener { ... override fun onCreate(savedInstanceState: Bundle?) { ... val adapter = HeroesAdapter(heroes, this) recyclerView.adapter = adapter } override fun onHeroClick(hero: Hero) { Log.d("MainActivity", "Clicked Hero: ${hero.name}") } }

4. Run the app and try clicking the heroes in the list. You should the hero names in your log

6. Add custom behavior when binding data through Binding Adapters

Put it simply, Binding Adapters are used to provide custom behaviors or functionalities before setting the data to View. A common example would be plurals where you either show the string in singular or plural.

Binding Adapters can be pretty handy for this case as you can reuse this functionality to any View. Let’s try it now but first:

1. Create a new class HeroVO

data class HeroVO(val id: String, val name: String, var matches: Int, val abilities: String)

2. Create new empty activity HeroActivity.class

3. Open your activity_hero.xml

<layout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto"> <data> <variable name="heroVO" type="com.imakeanapp.androiddatabindingsample.HeroVO" /> </data> <android.support.constraint.ConstraintLayout android:layout_width="match_parent" android:layout_height="match_parent"> <TextView android:id="@+id/hero_detail_matches" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginBottom="32dp" android:text="@{String.valueOf(heroVO.matches)}" android:textAppearance="@style/TextAppearance.AppCompat.Large" android:textStyle="bold" app:layout_constraintBottom_toTopOf="@+id/hero_detail_name" app:layout_constraintEnd_toEndOf="@+id/hero_detail_name" app:layout_constraintStart_toStartOf="@+id/hero_detail_name" app:layout_constraintTop_toTopOf="parent" app:layout_constraintVertical_chainStyle="packed" /> <TextView android:id="@+id/hero_detail_name" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginBottom="8dp" android:layout_marginEnd="8dp" android:layout_marginStart="8dp" android:text="@{heroVO.name}" android:textAppearance="@style/TextAppearance.AppCompat.Medium" app:layout_constraintBottom_toTopOf="@+id/hero_detail_abilities" app:layout_constraintEnd_toEndOf="@+id/hero_detail_abilities" app:layout_constraintStart_toStartOf="@+id/hero_detail_abilities" app:layout_constraintTop_toBottomOf="@+id/hero_detail_matches" /> <TextView android:id="@+id/hero_detail_abilities" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_marginEnd="8dp" android:layout_marginStart="8dp" android:text="@{heroVO.abilities}" android:textAlignment="center" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/hero_detail_name" /> </android.support.constraint.ConstraintLayout> </layout>

4. Open your HeroActivity.class

class HeroActivity : AppCompatActivity() { companion object { const val ARG_HERO_ID = "hero_id" } private lateinit var heroVO: HeroVO override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) val binding: ActivityHeroBinding = DataBindingUtil.setContentView(this, R.layout.activity_hero) val bundle = requireNotNull(intent.extras) val heroId = bundle.getString(ARG_HERO_ID, "") val repository = HeroesRepository.getInstance(applicationContext) repository.getHero(heroId)?.let { hero -> heroVO = HeroVO(hero.id, hero.name, hero.matches, hero.abilities) binding.heroVO = heroVO } } }

5. Open your MainActivity.class

class MainActivity : AppCompatActivity(), HeroesAdapter.OnHeroClickListener { ... override fun onHeroClick(hero: Hero) { val intent = Intent(this, HeroActivity::class.java) intent.putExtra(HeroActivity.ARG_HERO_ID, hero.id) startActivity(intent) } }

6. Run the app and you should now see the details of a hero in a different screen

Let’s now implement Binding Adapters!

7. Open your strings.xml file and add

<resources> <string name="app_name">AndroidDatabindingSample</string> <plurals name="matches"> <item quantity="one">%d Match</item> <item quantity="other">%d Matches</item> </plurals> </resources>

8. Create a new Kotlin file HeroBindingAdapter.kt

import android.databinding.BindingAdapter import android.widget.TextView @BindingAdapter("matchesText") fun matchesText(textView: TextView, matches: Int) { val resources = textView.context.resources textView.text = resources.getQuantityString(R.plurals.matches, matches, matches) }

9. Open your activity_hero.xml and in hero_detail_matches remove android:text=”…” and add

<?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"> <data> <variable name="heroVO" type="com.imakeanapp.androiddatabindingsample.HeroVO" /> </data> <android.support.constraint.ConstraintLayout android:layout_width="match_parent" android:layout_height="match_parent"> <TextView android:id="@+id/hero_detail_matches" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginBottom="32dp" android:textAppearance="@style/TextAppearance.AppCompat.Large" android:textStyle="bold" app:layout_constraintBottom_toTopOf="@+id/hero_detail_name" app:layout_constraintEnd_toEndOf="@+id/hero_detail_name" app:layout_constraintStart_toStartOf="@+id/hero_detail_name" app:layout_constraintTop_toTopOf="parent" app:layout_constraintVertical_chainStyle="packed" app:matchesText="@{heroVO.matches}"/> ... </android.support.constraint.ConstraintLayout> </layout>

10. Open your heroes.json file and change the matches of any hero to 1

11. Run the app and you should see your binding adapter working

7. Observing Data

By now you probably have an idea what an observable is. It’s the ability of an object to notify others that data has changed. Any plain-old object can be used for data binding but modifying the object doesn’t automatically update the UI.

The Data Binding library provides us the capability to make any plain-old object observable. Below is a list of Observable types in the library:

ObservableBoolean

ObservableByte

ObservableChar

ObservableShort

ObservableInt

ObservableLong

ObservableFloat

ObservableDouble

ObservableParcelable

ObservableField<T>

ObservableArrayMap

ObservableArrayList

1. Open your activity_hero.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"> <data> <variable name="heroVO" type="com.imakeanapp.androiddatabindingsample.HeroVO" /> </data> <android.support.constraint.ConstraintLayout android:layout_width="match_parent" android:layout_height="match_parent"> .... <Button android:id="@+id/increase_matches" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_marginBottom="8dp" android:layout_marginEnd="8dp" android:layout_marginStart="8dp" android:text="Increase Matches" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" /> </android.support.constraint.ConstraintLayout> </layout>

2. Let’s modify our HeroVO.class

data class HeroVO(val id: String, val name: String, var matches: ObservableInt, val abilities: String)

3. Open your HeroActivity.class

class HeroActivity : AppCompatActivity() { ... override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) ... repository.getHero(heroId)?.let { hero -> heroVO = HeroVO(hero.id, hero.name, ObservableInt(hero.matches), hero.abilities) binding.heroVO = heroVO binding.increaseMatches.setOnClickListener { heroVO.matches.set(heroVO.matches.get() + 1) } } } }

4. Run the app and click the button. You should the observable functionality working. The matches just increase without modifying the View 🎉

That’s it! Congratulations for reaching this far! 🎉

By now you should have a basic understanding on how you can implement Data Binding in Android.

Check Android’s Data Binding documentation for more details

Want to learn more about Android Jetpack?

Checkout all my tutorials related to Android Jetpack.

If you find this article helpful, you can subscribe below to be updated with new useful articles in the future.