LiveData and Data Binding
Android’s data binding framework offers a way for you to push data into your widgets with less code, by “binding” sources of data to those widgets via special XML attributes in layout resources. The data binding framework was created before the Architecture Components existed, and so originally the data binding framework had no support for LiveData
. Now, though, it does, and so you can have your UI update itself automatically from LiveData
sources, if you choose.
In this chapter, we will explore how to set this up.
A Data Binding Recap
Extensive coverage of data binding can be found in The Busy Coder’s Guide to Android Development. However, here is a brief reminder of what data binding is and how it works.
Note that data binding is not automatically used in an Android project. You need to enable it via dataBinding { enabled true }
in the android
closure of your module’s build.gradle
file.
New Layout Resource Structure
Classic Android layout resources are purely a view hierarchy. Typically, the root XML element then is the outermost container class (e.g., a ConstraintLayout
).
A layout resource that will participate in data binding will instead have a root <layout>
element. That will have two child elements:
- A
<data>
element with metadata for the data binding - The outermost container class — what would have been the root element before
Those need to appear in that order, with <data>
as the first child.
A <data>
element can have a variety of child elements to configure the data binding, but the most important ones are <variable>
elements. These declare what objects are being bound to this layout, and they provide both the name
and the Java/Kotlin type
of the object:
<layout>
<data>
<variable
name="viewModel"
type="com.commonsware.android.livedata.SensorViewModel" />
</data>
<!-- view hierarchy goes here -->
</layout>
Binding Expressions
The XML attributes of the views can then reference those variables in binding expressions. Rather than having a simple value (e.g., android:text="Hello, world!"
), a binding expression contains references to variables (plus public
fields and methods on those variables) and other view attributes, with a simple expression syntax. The data binding framework identifies these expressions via @{}
syntax (android:text="@{viewModel.sensorReading}"
).
Adapters
Sometimes, the data types available to binding expressions do not quite match the data type needed by the XML attribute. For example, android:text
maps to setText()
on TextView
, and that needs either an int
resource ID or a CharSequence
(e.g., String
) value. If you have something else — such as a SensorLiveData.Event
object — you will need to provide a “binding adapter” to help bridge the gap.
These adapters come in the form of static
methods annotated with @BindingAdapter
:
@BindingAdapter("android:text")
public static void setLightReading(TextView tv, SensorLiveData.Event event) {
if (event==null) {
tv.setText(null);
}
else {
tv.setText(String.format("%f", event.values[0]));
}
}
The annotation argument is the name of the XML attribute to which this adapter applies. So, in this case, if we try binding a SensorLiveEvent.Data
object into a TextView
via its android:text
attribute, this method will be called. It is up to us then to do something useful to fulfill that binding — in this case, we call setText()
on the TextView
with a suitable value, based on the event
.
Binding from Code
Somewhere, though, we need to supply the values for those variables. That comes from using a different way to set up this layout. Rather than using setContentView()
or a LayoutInflater
directly, we use a code-generated class, created by the tools associated with the data binding framework. This class not only takes care of inflating the layout for us, but it gives us setter methods to supply the variables.
The name of this code-generated class is based on the name of the layout resource:
- The filename, without the extension, is coverted into CamelCase
-
Binding
is appended
So, main.xml
turns into MainBinding
, activity_main.xml
turns into ActivityMainBinding
, and so on.
The binding class has a static
method named inflate()
that takes a LayoutInflater
and returns an instance of the binding class (like a factory method would), inflating the underlying layout along the way:
MainBinding binding=MainBinding.inflate(getLayoutInflater());
It has a getRoot()
method that returns the root View
of your view hierarchy, for use with setContentView()
, onCreateViewHolder()
, etc. And it has methods for each one of your variables, where the method name is derived from the variable name:
- The variable name is converted to CamelCase
-
set
is appended to the front
So, a viewModel
variable results in a setViewModel()
method. Calling this on the binding triggers the evaluation of the binding expressions and populates the attributes of the affected widgets.
Observable Data Sources
If the variables implement the Observable
interface, then once you have attached the variables’ objects to the binding, changes to the variables’ values automatically re-evaluate the binding expressions. You do not need to do anything else.
For simple primitives, there are a series of Observable
implementations, such as ObservableBoolean
and ObservableInt
. For String
and arbitrary other objects, there is a generic ObservableField
that you can use.
We will see an example of this shortly, as this is where LiveData
starts becoming important.
Prev Table of Contents Next
This book is licensed under the Creative Commons Attribution-ShareAlike 4.0 International license.