In the chapter entitled Modern Android App Architecture with Jetpack, we introduced the concept of Android Data Binding. We explained how it is used to directly connect the views in a user interface layout to the methods and data located in other objects within an app without the need to write code. This chapter will provide more details on data binding, emphasizing how data binding is implemented within an Android Studio project. The tutorial in the next chapter (An Android Studio Data Binding Tutorial) will provide a practical example of data binding in action.
An Overview of Data Binding
The Android Jetpack Data Binding Library provides data binding support, primarily providing a simple way to connect the views in a user interface layout to the data stored within the app’s code (typically within ViewModel instances). Data binding also provides a convenient way to map user interface controls, such as Button widgets, to event and listener methods within other objects, such as UI controllers and ViewModel instances.
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 that this can be achieved with no code beyond that necessary to initially set up the binding.
Connecting an interactive view, such as a Button widget, to a method within a UI controller traditionally required that the developer write code to implement a listener method to be called when the button is clicked. Data binding makes this as simple as referencing the method to be called within the Button element in the layout XML file.
The Key Components of Data Binding
An Android Studio project is not configured for data binding support by default. Several elements must be combined before an app can begin using data binding. These involve the project build configuration, the layout XML file, data binding classes, and the use of the data binding expression language. While this may appear overwhelming at first, when taken separately, these are quite simple steps that, once completed, are more than worthwhile in terms of saved coding effort. Each element will be covered in detail in the remainder of this chapter. Once these basics have been covered, the next chapter will work through a detailed tutorial demonstrating these steps.
The Project Build Configuration
Before a project can use data binding, it must be configured to use the Android Data Binding Library and to enable support for data binding classes and the binding syntax. Fortunately, this can be achieved with just a few lines added to the module level build.gradle.kts file (the one listed as build.gradle.kts (Module: app) under Gradle Scripts in the Project tool window). The following lists a partial build file with data binding enabled:
.
.
android {
buildFeatures {
dataBinding = true
}
.
.
Code language: HTML, XML (xml)
The Data Binding Layout File
As we have seen in previous chapters, the user interfaces for an app are typically contained within an XML layout file. Before the views contained within one of these layout files can take advantage of data binding, the layout file must be converted to a data binding layout file.
As outlined earlier in the book, XML layout files define the hierarchy of components in the layout, starting with a top-level or root view. Invariably, this root view takes the form of a layout container such as a ConstraintLayout, FrameLayout, or LinearLayout instance, as is the case in the fragment_main.xml file for the ViewModelDemo project:
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.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:id="@+id/main"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".ui.main.MainFragment">
.
.
</androidx.constraintlayout.widget.ConstraintLayout>
Code language: HTML, XML (xml)
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.
In the case of the above example, this would require that the following changes be made to the existing layout file:
<?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">
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/main"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".ui.main.MainFragment">
.
.
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
Code language: HTML, XML (xml)
The Layout File Data Element
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 need a variable name 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="com.ebookfrenzy.myapp.ui.main.MainViewModel" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/main"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".ui.main.MainFragment">
.
.
</layout>
Code language: HTML, XML (xml)
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 MyViewModel class when declaring the variable).
The data element can import other classes that may then be referenced within binding expressions elsewhere in the layout file. For example, if you have a class containing a method that needs to be called on a value before it is displayed to the user, the class could be imported as follows:
<data>
<import type="com.ebookfrenzy.MyFormattingTools" />
<variable
name="viewModel"
type="com.ebookfrenzy.myapp.ui.main.MainViewModel" />
</data>
Code language: HTML, XML (xml)
The Binding Classes
For each class referenced in the data element within the binding layout file, Android Studio will automatically generate a corresponding binding class. This subclass of the Android ViewDataBinding class will be named based on the layout filename using word capitalization and the Binding suffix. Therefore, the binding class for a layout file named fragment_main.xml file will be named FragmentMainBinding. 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 must 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 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)
Code language: Kotlin (kotlin)
In the case of a Fragment, this takes place in the onCreateView() method:
return inflater.inflate(R.layout.fragment_main, container, false)
Code language: Kotlin (kotlin)
All that is needed to create the binding class instances within an Activity class is to modify this initialization code as follows:
lateinit var binding: ActivityMainBinding
binding = DataBindingUtil.inflate(
inflater, R.layout.activity_main, container, false)
Code language: Kotlin (kotlin)
In the case of a Fragment, the code would read as follows:
lateinit var binding: FragmentMainBinding
binding = DataBindingUtil.inflate(
inflater, R.layout.fragment_main, container, false)
binding.setLifecycleOwner(this)
return binding.root
Code language: Kotlin (kotlin)
Data Binding Variable Configuration
As outlined above, the data binding layout file contains the data element, which contains variable elements consisting of variable names and the class types to which the bindings are to be established. For example:
<data>
<variable
name="viewModel"
type="com.ebookfrenzy.viewmodeldemo.ui.main.MainViewModel" />
<variable
name="uiController"
type="com.ebookfrenzy.viewmodeldemo_databinding.ui.main.MainFragment" />
</data>
Code language: HTML, XML (xml)
In the above example, the first variable knows that it will be binding to an instance of a ViewModel class of type MainViewModel but has yet to be connected to an actual MainViewModel object instance. This requires the additional step of assigning the MainViewModel instance used within the app to the variable declared in the layout file. This is performed via a call to the setVariable() method of the data binding instance, a reference to which was obtained in the previous chapter:
var MainViewModel mViewModel =
ViewModelProvider(this).get(MainViewModel::class.java)
binding.setVariable(mViewModel, viewModel)
Code language: Kotlin (kotlin)
The second variable in the above data element references a UI controller class in the form of a Fragment named MainFragment. In this situation, the code within a UI controller (be it an Activity or Fragment) would need to assign itself to the variable as follows:
binding.setVariable(uiController, this)
Code language: Kotlin (kotlin)
Binding Expressions (One-Way)
Binding expressions define how a particular view interacts with bound objects. For example, a binding expression on a Button 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.
Binding expressions use a declarative language that allows logic and access to other classes and methods to decide 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 standard Java language libraries are imported by default, so many things that can be achieved in Java or Kotlin can also be performed in a binding expression. As already discussed, the data element may also be used to import custom classes to add more capability to expressions.
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 were stored as a String object, this would be declared within the layout file as follows:
<TextView
android:id="@+id/resultText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{viewModel.result}"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
Code language: HTML, XML (xml)
In the above XML, the text property is set to the value stored in the result LiveData property of the viewModel object.
Consider, however, that the result is stored within the model as a Float value instead of a String. That being the case, the above expression would cause a compilation error. Clearly, the Float value must be converted to a string before the TextView can display it. To resolve issues such as this, the binding expression can include the necessary steps to complete the conversion using the standard Java language classes:
android:text="@{String.valueOf(viewModel.result)}"
Code language: HTML, XML (xml)
When running the app after making this change, it is important to be aware that the following warning may appear in the Android Studio console:
warning: myViewModel.result.getValue() is a boxed field but needs to be un-boxed to execute String.valueOf(viewModel.result.getValue()).
Code language: plaintext (plaintext)
Values in Java can take the form of primitive values such as the boolean type (referred to as being unboxed) or wrapped in a Java object such as the Boolean type and accessed via reference to that object (i.e., boxed). The unboxing process involves unwrapping the primitive value from the object.
To avoid this message, wrap the offending operation in a safeUnbox() call as follows:
android:text="@{String.valueOf(<strong>safeUnbox(</strong>myViewModel.result)<strong>)</strong>}"
Code language: HTML, XML (xml)
String concatenation may also be used. For example, to include the word “dollars” after the result string value, the following expression would be used:
android:text='@{String.valueOf(safeUnbox(myViewModel.result)) + " dollars"}'
Code language: HTML, XML (xml)
Note that since the appended result string is wrapped in double quotes, the expression is now encapsulated with single quotes to avoid syntax errors.
The expression syntax also allows ternary statements to be declared. In the following expression, the view will display different text depending on whether or not the result value is greater than 10.
@{myViewModel.result > 10 ? "Out of range" : "In range"}
Code language: HTML, XML (xml)
Expressions may also be constructed to access specific elements in a data array:
@{myViewModel.resultsArray[3]}
Code language: HTML, XML (xml)
Binding Expressions (Two-Way)
The type of expression covered so far is called one-way binding. In other words, 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.
A two-way binding, on the other hand, 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. When declaring a two-way expression, the syntax is similar to a one-way expression except that it begins with @=. For example:
android:text="@={myViewModel.result}"
Code language: HTML, XML (xml)
Event and Listener Bindings
Binding expressions may also trigger method calls in response to events on a view. A Button view, for example, can be configured to call a method when clicked. In the chapter entitled An Android Studio Tutorial, for example, the onClick property of a button was configured to call a method within the app’s main activity named convertCurrency(). Within the XML file, this was represented as follows:
android:onClick="convertCurrency"
Code language: HTML, XML (xml)
The convertCurrency() method was declared along the following lines:
fun convertCurrency(view: View) {
.
.
}
Code language: Kotlin (kotlin)
Note that this type of method call is always passed a reference to the view on which the event occurred. The same effect 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}"
Code language: HTML, XML (xml)
Another option, and one which provides the ability to pass parameters to the method, is referred to as a listener binding. The following expression uses this approach to call a method on the same viewModel instance with no parameters:
android:onClick='@{() -> myViewModel.methodOne()}'
Code language: HTML, XML (xml)
The following expression calls a method that expects three parameters:
android:onClick='@{() -> myViewModel.methodTwo(viewModel.result, 10, "A String")}'
Code language: HTML, XML (xml)
Binding expressions provide a rich and flexible language to bind user interface views to data and methods in other objects. This chapter has only covered the most common use cases. To learn more about binding expressions, review the Android documentation online at:
https://developer.android.com/topic/libraries/data-binding/expressions
Summary
Android data bindings provide a system for creating connections between the views in a user interface layout and the data and methods of other objects within the app architecture without writing code. Once some initial configuration steps have been performed, data binding involves using binding expressions within the view elements of the layout file. These binding expressions can be either one-way or two-way and may also be used to bind methods to be called in response to events such as button clicks within the user interface.