An Overview of Android Jetpack Data Binding
In the chapter entitled TBD, we introduced the concept of Android Data Binding and briefly 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 with an emphasis on explaining how data binding is implemented within an Android Studio project. The tutorial in the next chapter (TBD) will provide a practical example of data binding in action.
An Overview of 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 an convenient way to map user interface controls such as Button widgets to event and listener methods within other objects such as UI controllers and ViewModels 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 in 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.
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
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. While this may appear to be a little overwhelming at first, when taken separately these are actually quite simple steps which, once completed, are more than worthwhile in terms of saved coding effort. In the remainder of this chapter, each of these elements will be covered in detail. Once these basics have been covered, the next chapter will work through a detailed tutorial demonstrating these steps in practical terms.
The Project Build Configuration
Before a project can make use of data bindings 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 (the one listed as build.gradle (Module: app) under Gradle Scripts in the Project tool window). The following lists a partial build file with data binding enabled:
apply plugin: 'com.android.application' android { compileSdkVersion 'android-P' dataBinding { enabled = true } defaultConfig { applicationId "com.ebookfrenzy.myapplication" . .
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 file can take advantage of data binding, the layout file must first 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 main_fragment.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>
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.
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 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> </layout>
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 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="viewModel" type="com.ebookfrenzy.myapp.ui.main.MyViewModel" /> </data> <android.support.constraint.ConstraintLayout android:id="@+id/main" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".ui.main.MainFragment"> . . </layout>
The above data element declares a new variable named viewModel of type MyViewModel (note that it is necessary to declare the full package name of the MyViewModel class when declaring the variable).
The data element can also 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.MyViewModel" /> </data>
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 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);
In the case of a Fragment, this takes place in the onCreateView() method:
return inflater.inflate(R.layout.main_fragment, container, false);
All that is needed to create the binding class instances within an Activity class is to modify this initialization code as follow:
MainFragmentBinding binding; binding = DataBindingUtil.setContentView(this, R.layout.activity_main)
In the case of a Fragment, the code would read as follows:
MainFragmentBinding binding; binding = DataBindingUtil.inflate( inflater, R.layout.main_fragment, container, false); binding.setLifecycleOwner(this); View view = binding.getRoot(); return view;
Data Binding Variable Configuration
As outlined above, the data binding layout file contains the data element which contains a variables 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>
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 not yet been 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:
MainViewModel myViewModel = ViewModelProviders.of(this).get(MainViewModel.class); binding.setVariable(viewModel, myViewModel);
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 a Activity or Fragment) would simply need to assign itself to the variable as follows:
binding.setVariable(uiController, this);
Binding Expressions (One-Way)
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.
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. As already discussed, the data element may also be used to import custom classes to add yet 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 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: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" />
In the above XML the text property is being 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 will need to 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 languages classes:
android:text="@{String.valueOf(viewModel.result)}"
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: viewModel.result.getValue() is a boxed field but needs to be un-boxed to execute String.valueOf(viewModel.result.getValue()).
Values in Java can take the form of primitive values such as the boolean type (referred to as being unboxed) or wrapped in an Java object such as the Boolean type and accessed via reference to that object (i.e. boxed). The process of unboxing involves the unwrapping of the primitive value from the object.
To avoid this message, wrap the offending operation in a safeUnbox() call as follows:
android:text="@{String.valueOf(safeUnbox(viewModel.result))}"
String concatenation may also be used. For example, to includes the word “dollars” after the result string value the following expression would be used:
android:text='@{String.valueOf(safeUnbox(viewModel.result)) + " dollars"}'
Note that since the appended euros 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.
@{viewModel.result > 10 ? “Out of range” : “In range”}
Expressions may also be constructed to access specific elements in a data array:
@{viewModel.resultsArray[3]}
Binding Expressions (Two-Way)
The type of expressions covered so far are referred to as a 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 with the exception that it begins with @=. For example:
android:text="@={viewModel.result}"
Event and Listener Bindings
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. Back in the chapter entitled TBD, 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”
The convertCurrency() method was declared along the following lines:
public void convertCurrency(View view) { . . }
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}”
When working with Fragments, the above expression can be used to replace all of the following listener code:
convertButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { } });
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='@{() -> viewModel.methodOne()}'
The following expression calls a method that expects three parameters:
android:onClick='@{() -> viewModel.methodTwo(viewModel.result, 10, “A String”)}'
Binding expressions provide a rich and flexible language in which to bind user interface views to data and methods in other objects and 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 having to write code. Once some initial configuration steps have been performed, data binding simply involves the use of 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 a button clicks within the user interface.