An Android Jetpack ViewModel LiveData Tutorial using Kotlin

Overview of components

Android Jetpack consists of Android Studio, the Android Architecture Components and Android X together with a set of guidelines that recommend how an Android App should be structured. The Android Architecture Components are designed to make it quicker and easier both to perform common tasks when developing Android apps while also conforming to the key principle of the architectural guidelines.

The purpose of ViewModel is to separate the user interface-related data model and logic of an app from the code responsible for actually displaying and managing the user interface and interacting with the operating system. When designed in this way, an app will consist of one or more UI Controllers, such as an activity, together with ViewModel instances responsible for handling the data needed by those controllers.

In effect, the ViewModel only knows about the data model and corresponding logic. It knows nothing about the user interface and makes no attempt to directly access or respond to events relating to views within the user interface. When a UI controller needs data to display, it simply asks the ViewModel to provide it. Similarly, when the user enters data into a view within the user interface, the UI controller passes it to the ViewModel for handling.

LiveData is a data holder that allows a value to become observable. In basic terms, an observable object has the ability to notify other objects when changes to its data occur thereby solving the problem of making sure that the user interface always matches the data within the ViewModel. This means, for example, that a UI controller that is interested a ViewModel value can set up an observer which will, in turn, be notified when that value changes.

Android Jetpack includes the Data Binding Library which allows data in a ViewModel to be mapped directly to specific views within the XML user interface layout file. Data binding allows the LiveData value stored in the ViewModel to be referenced directly within the XML layout file avoiding the need to write code to keep the layout views updated.

This tutorial will implement the same currency converter app, this time using the ViewModel component and following the Google app architecture guidelines to avoid Activity lifecycle complications.

ViewModel

First of all add lifecycle-extensions to the build.gradle file

dependencies {
    ...
    implementation 'androidx.lifecycle:lifecycle-extensions:2.0.0'
}

VMViewModel class is declared as a sub-class of the ViewModel Android architecture component class and is ready to be modified to store the data model for the app.

class VMViewModel: ViewModel() {
    private val USD_TO_EU_RATE = 0.74f
    private var dollarText = ""
    private var result: Float? = 0f

    fun setAmount(value: String) {
        this.dollarText = value
        result = dollarText.toFloat() * USD_TO_EU_RATE
    }

    fun getResult(): Float? {
        return result
    }
}

Clearly, there needs to be some way for the activity to obtain a reference to the ViewModel in order to be able to access the model and observe data changes. A Fragment or Activity maintains references to the ViewModels on which it relies for data using an instance of the ViewModelProvider class.

A ViewModelProvider instance is created via a call to the ViewModelProviders.of() method from within the Activity. When called, the method is passed a reference to the current Fragment or Activity and returns a ViewModelProvider instance as follows:

val viewModelProvider: ViewModelProvider = ViewModelProviders.of(activity)

Once the ViewModelProvider instance has been created, the get() method can be called on that instance passing through the class of specific ViewModel that is required. The provider will then either create a new instance of that ViewModel class, or return an existing instance:

val viewModel: VMViewModel = viewModelProvider.get(VMViewModel::class.java)

Following is full code for a Activity

class VMActivity : AppCompatActivity() {
    val vm: VMViewModel by lazy { ViewModelProviders.of(this).get(VMViewModel::class.java) }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_vm)

        tv.text = vm.getResult().toString()

        btn.setOnClickListener {
            if (et.text.isNotEmpty()) {
                vm.setAmount(et.text.toString())
                tv.text = vm.getResult().toString()
            } else {
                tv.text = "No value"
            }
        }
    }
}

Following is content of activity_vm.xml layout

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
        xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".VMActivity">
    <TextView
            android:text="TextView"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content" 
            app:layout_constraintTop_toTopOf="parent"
            app:layout_constraintStart_toStartOf="parent" 
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent" android:id="@+id/tv"/>
    <Button
            android:text="Button"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            app:layout_constraintStart_toStartOf="parent" 
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintTop_toBottomOf="@id/tv"
            android:id="@+id/btn" 
            android:layout_marginRight="8dp" 
            android:layout_marginEnd="8dp"
            android:layout_marginLeft="8dp" 
            android:layout_marginStart="8dp" 
            android:layout_marginTop="32dp"
            android:visibility="visible"/>
    <EditText
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:inputType="number"
            android:ems="10"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintEnd_toEndOf="parent" 
            android:id="@+id/et"
            app:layout_constraintBottom_toTopOf="@+id/tv" 
            android:layout_marginBottom="32dp"/>
</androidx.constraintlayout.widget.ConstraintLayout>

LiveData

LiveData component can be used as a wrapper around data values within a view model. Once contained in a LiveData instance, those variables become observable to other objects within the app, typically UI controllers such as Activities and Fragments. This allows the UI controller to receive a notification whenever the underlying LiveData value changes. An observer is set up by creating an instance of the Observer class and defining an onChange() method to be called when the LiveData value changes. Once the Observer instance has been created, it is attached to the LiveData object via a call to the LiveData object’s observe() method.

LiveData instances can be declared as being mutable using the MutableLiveData class, allowing both the ViewModel and UI controller to make changes to the underlying data value.

Edit the VMViewModel.kt file and wrap the result variable in a MutableLiveData instance

class VMViewModel: ViewModel() {
    private val USD_TO_EU_RATE = 0.74f
    private var dollarText = ""
    private var result: MutableLiveData<Float> = MutableLiveData<Float>()
    fun setAmount(value: String) {
        this.dollarText = value
        result.value = dollarText.toFloat() * USD_TO_EU_RATE
    }

    fun getResult(): MutableLiveData<Float> {
        return result
    }
}

Next we have to update VMActivity.kt file. The resultObserver instance declares the onChanged() method which, when called, is passed the current result value which it then converts to a string and displays on the result TextView object. The next step is to add the observer to the result LiveData object, a reference to which can be obtained via a call to the getResult() method of the ViewModel object. Since updating the result TextView is now the responsibility of the onChanged() callback method, the existing lines of code to perform this task can now be deleted:

class VMActivity : AppCompatActivity() {
    val vm: VMViewModel by lazy { ViewModelProviders.of(this).get(VMViewModel::class.java) }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_vm)

        val resultObserver: Observer<Float> = Observer {
            tv.text = it.toString()
        }
        vm.getResult().observe(this, resultObserver)

        btn.setOnClickListener {
            if (et.text.isNotEmpty()) {
                vm.setAmount(et.text.toString())
            } else {
                tv.text = "No value"
            }
        }
    }
}

Data Binding

Data binding support is provided by the Android Jetpack Data Binding Library, the primary purpose of which is to provide a simple way to connect the views in a user interface layout to the data that is stored within the code of the app (typically within ViewModel instances). Data binding also provides a convenient way to map user interface controls such as Button widgets to event.

Data binding becomes particularly powerful when used in conjunction with the LiveData component. Consider, for example, an EditText view bound to a LiveData variable within a ViewModel using data binding. When connected in this way, any changes to the data value in the ViewModel will automatically appear within the EditText view and, when using two-way binding, any data typed into the EditText will automatically be used to update the LiveData value. Perhaps most impressive is the fact that this can be achieved with no code beyond that necessary to initially set up the binding.

By default, an Android Studio project is not configured for data binding support. In fact, a number of different elements need to be combined before an app can begin making use of data binding. These involve the project build configuration, the layout XML file, data binding classes and use of the data binding expression language.

Before a project can make use of data binding it must first be configured to make use of the Android Data Binding Library and to enable support for data binding classes and the binding expression syntax. Fortunately this can be achieved with just a few lines added to the module level build.gradle file.

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

In order to be able to use data binding, the layout hierarchy must have a layout component as the root view which, in turn, becomes the parent of the current root view.

Made following changes to the existing layout

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    xmlns:android="http://schemas.android.com/apk/res/android">
    <android.support.constraint.ConstraintLayout
    android:id="@+id/main"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".ui.main.MainFragment">
    ...
    </android.support.constraint.ConstraintLayout>
</layout>

The data binding layout file needs some way to declare the classes within the project to which the views in the layout are to be bound (for example a ViewModel or UI controller). Having declared these classes, the layout file will also need a variable name by which to reference those instances within binding expressions.

This is achieved using the data element, an example of which is shown below:

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    xmlns:android="http://schemas.android.com/apk/res/android">

    <data>
    <variable
        name="myViewModel"
        type="me.proft.theapp.ui.main.MainViewModel" />
    </data>
    <android.support.constraint.ConstraintLayout
    android:id="@+id/main"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".ui.main.MainFragment">
    ...
    </android.support.constraint.ConstraintLayout>
</layout>

The above data element declares a new variable named myViewModel of type MainViewModel (note that it is necessary to declare the full package name of the VMViewModel class when declaring the variable).

For each class referenced in the data element within the binding layout file, Android Studio will automatically generate a corresponding binding class . This is a subclass of the Android ViewDataBinding class and will be named based on the layout filename using word capitalization and the Binding suffix. The binding class for a layout file named main_fragment.xml file, therefore, will be named MainFragmentBinding. The binding class contains the bindings specified within the layout file and maps them to the variables and methods within the bound objects.

Although the binding class is generated automatically, code still needs to be written to create an instance of the class based on the corresponding data binding layout file. Fortunately, this can be achieved by making use of the DataBindingUtil class.

The initialization code for an Activity or Fragment will typically set the content view or inflate the user interface layout file. This simply means that the code opens the layout file, parses the XML and creates and configures all of the view objects in memory. In the case of an existing Activity class, the code to achieve this can be found in the onCreate() method and will read as follows:

setContentView(R.layout.activity_main)

All that is needed to create the binding class instances within an Activity class is to modify this initialization code as follows:

val binding: MainFragmentBinding = DataBindingUtil.setContentView(this, R.layout.activity_main)

Binding expressions define how a particular view interacts with bound objects. A binding expression on a Button, for example, might declare which method on an object is called in response to a click. Alternatively, a binding expression might define which data value stored in a ViewModel is to appear within a TextView and how it is to be presented and formatted.

There are two types of Binding expressions

  • One-way binding. The layout is constantly updated as the corresponding value changes, but changes to the value from within the layout do not update the stored value.
  • Two-way binding. It allows the data model to be updated in response to changes in the layout. An EditText view, for example, could be configured with a two-way binding so that when the user enters a different value, that value is used to update the corresponding data model value.

Binding expressions use a declarative language that allows logic and access to other classes and methods to be used in deciding how bound data is used. Expressions can, for example, include mathematical expressions, method calls, string concatenations, access to array elements and comparison operations. In addition, all of the standard Java language libraries are imported by default so many things that can be achieved in Java can also be performed in a binding expression.

A binding expression begins with an @ symbol followed by the expression enclosed in curly braces {}.

Consider, for example, a ViewModel instance containing a variable named result. Assume that this class has been assigned to a variable named viewModel within the data binding layout file and needs to be bound to a TextView object so that the view always displays the latest result value. If this value was stored as a String object, this would be declared within the layout file as follows:

<TextView
android:id="@+id/resultText"
android:text="@{viewModel.result}
.../>

When declaring a two-way expression, the syntax is similar to a one-way expression with the exception that it begins with @=. For example

android:text="@={myViewModel.result}

Binding expressions may also be used to trigger method calls in response to events on a view. A Button view, for example, can be configured to call a method when clicked. This can be achieved in data binding using the following expression (assuming the layout has been bound to a class with a variable name of uiController)

android:onClick="@{uiController::convertCurrency}