Android Data Binding With LiveData (Kotlin)

October 26, 2018

Why Use Data Binding?

Usually I am not a fan of data binding, as there are too many “magic” (hard to debug).

One thing I like though is two-way binding, where the value changed in EditText is updated to the backing data/variable. Basically I have reactivity like React/Vue.js, where UI input changes the data automatically and vice versa.

Android’s data binding is pretty flexible as well, supporting evaluations and expressions (e.g. android:transitionName='@{"Hello " + name}') or use data binding to change visibility (e.g. android:visibility="@{age < 13 ? View.GONE : View.VISIBLE}").

If you need something more complex there are adapters, but I prefer not to introduce so much complexity.

Setup Data Binding

Configuration

Edit app module build.gradle.

android {
    ...
    dataBinding {
        enabled = true
    }
}

Edit gradle.properties.

android.databinding.enableV2=true

Layout

Change the layout file of Activity/Fragment.

  • Adding <layout> as root tag.
  • Add <data> tag for variables to be used in data binding
  • Use android:text='@{VARIABLE_NAME}' to show the value
<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="viewModel" type="com.luasoftware.pixpin.view.AlbumViewViewModel"/>
        <variable name="test" type="String" />
    </data>

    <android.support.design.widget.CoordinatorLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".view.AlbumViewActivity">

        ...

        <TextView
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_gravity="bottom|end"
            android:text='@{test}'
            />

    </android.support.design.widget.CoordinatorLayout>

If you are using <include> tag, do the following to pass the variable.

<include layout="@layout/albumview_content" app:viewModel="@{viewModel}" />

You need to declare <layout> and <data> in albumview_content.xml as well.

<?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="viewModel" type="com.luasoftware.pixpin.view.AlbumViewViewModel"/>
    </data>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical"
        app:layout_behavior="@string/appbar_scrolling_view_behavior">

        <TextView
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="@{viewModel.liveItem.name}"/>

        <EditText
            android:id="@+id/testEditText"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="@={viewModel.liveItem.name}"/>
    </LinearLayout>
</layout>   

Code (Activity, ViewModel, LiveData)

Edit Activity class.

  • Call DataBindingUtil.setContentView at onCreate replacing setContentView, and binding.setLifecycleOwner (If you are using LiveData for data binding, not necessary if you are using observable).
  • Assign variable for data binding through binding.VARIABLE_NAME = ....
  • Observe changes on viewModel.liveItem.name (used for data binding), and update viewModel.item if changes detected
class AlbumViewActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        // replace setContentView with DataBindingUtil.setContentView
        // setContentView(R.layout.albumview_activity)

        // AlbumviewActivityBinding is auto generated based on the layout file (albumview_activity.xml)
        // val binding = DataBindingUtil.setContentView<AlbumviewActivityBinding>(this, R.layout.albumview_activity)
        val binding = AlbumviewActivityBinding.inflate(layoutInflater)
        binding.setLifecycleOwner(this) // must call this

        // I am using Dagger2 to get ViewModel, you can stick with whatever way you are using
        viewModel = ViewModelProviders.of(this, viewModelFactory).get(AlbumViewViewModel::class.java)

        // assign variable for data binding
        binding.viewModel = viewModel
        binding.test = "Hello World"

        setupObserver()

        // create data
        val item = Album(name = "Test", count=0)
        viewModel.copyItemToLive(item)
    }

    private fun setupObserver() {
        // make sure changed on viewModel.liveItem (used for data binding) will update viewModel.item
        // viewModel.item is the true source used for saving the data
        viewModel.liveItem.name.observe(this, Observer {
            it?.also {
                viewModel.item.name = it
            }
        })
    }
}

Code of ViewModel.

  • I use MutableLiveData for two way binding (UI input will update data, and vice versa)
  • item is the actual data model (true source used for data saving), liveItem is the reactive model (a bridge between UI input and the actual data model - item)

NOTE: Binding to LiveData require Android Studio 3.1 with com.android.tools.build:gradle:3.1.x

data class Album(val name: String, val count: Int = 0)

// I remove my dagger2 injection code
// class AlbumViewViewModel @Inject constructor(private val dataSource: AlbumDao): ViewModel() {
class AlbumViewViewModel : ViewModel() {
    inner class LiveItem {
        val name = MutableLiveData<String>()
    }

    var item: Album = Album(name="Original")
    val liveItem: LiveItem = LiveItem()

    fun copyItemToLive(item: Album) {
        // store data
        this.item = item
        // copy to liveItem
        liveItem.apply {
            name.value = item.name
        }
    }

    fun save() {
        // save item to database
    }
}

Assumption and behaviour

  • If changes is made to viewModel.item (for this case only name is relavant, as count is not used for data binding), you need to call copyItemToLive to make sure viewModel.liveItem has the same value.
  • Changes to viewModel.item.name will auto update viewModel.item with the setupObserver code. If you don’t need real-time update, you can skip the observer and copy viewModel.liveItem to viewModel.item when saving the data.

Layout Binding

test is a regular binding (android:text='@{test}') to a string value (it can be an object as well).

<variable name="test" type="String" />
<TextView
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_gravity="bottom|end"
    android:text='@{test}'
    />

viewModel.liveItem.name is a regular binding (android:text="@{viewModel.liveItem.name}) to a LiveData.

Whenever the value of LiveData changed, the view shall be updated.

You can observe viewModel.liveItem.name for changes.

<variable name="viewModel" type="com.luasoftware.pixpin.view.AlbumViewViewModel"/>
class AlbumViewViewModel : ViewModel() {
    inner class LiveItem {
        val name = MutableLiveData<String>()
    }

    val liveItem: LiveItem = LiveItem()
}
<TextView
    android:id="@+id/testTextView"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:text="@{viewModel.liveItem.name}"/>

viewModel.liveItem.name (a LiveData) can be used for 2-way binding (android:text="@={viewModel.liveItem.name}").

Whenever the EditText value changed, viewModel.liveItem.name is updated (if viewModel.liveItem.name is binded to another TextView, TextView value will change as well).

<EditText
    android:id="@+id/testEditText"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:text="@={viewModel.liveItem.name}"/>

References:

This work is licensed under a
Creative Commons Attribution-NonCommercial 4.0 International License.