From Punched Cards to Prompts
AndroidIntroduction When computer programming was young, code was punched into cards. That is, holes were punched into a piece of cardboard in a format...
Last year, the Android team introduced a new building block for Android development: the Data Binding framework.
Data Binding is supposed to eliminate layout-related boilerplate.
If you’re a practicing Android developer, you might not have leaped on this.
Odds are good that you’re already using a view injection library like ButterKnife, which mitigates the pain Data Binding is meant to address.
And Data Binding does so much more than ButterKnife — maybe it’s not worth the additional complexity and mystery of a giant new tool.
In this post, I’d like to dive into Data Binding with fresh eyes.
I’ll explain how Data Binding works at development time and at build time,
which should clarify some of the rough edges in the current pre-release version.
By the end, I hope to get you to where you can make an informed decision as to whether you want to use Data Binding, as well as how much of it you want to make use of.
For the examples in this post, I will use an example app that is familiar to me: CriminalIntent, from our book Android Programming: The Big Nerd Ranch Guide.
If you’re familiar with the book, I am basing my work off of the solution to Chapter 13 in the 2nd edition.
I’ve modified it slightly to illustrate a few ideas here, and to provide some sample data to play with.
If you want to follow along, clone data-binding-talk and checkout the branch jingibus/starting-point
.
CriminalIntent is a master-detail app: the main screen (implemented in CrimeListFragment.java
shows a list of workplace crimes, each with a few different properties.
If you tap on a crime, the app pulls up a detail screen to edit the crime in question.
We’ll be playing around with Data Binding in the main list screen:
Make sure you have the latest version of Android Studio 2.
You’ll also need to update your Gradle version.
Newer versions of Android Studio automatically ask you to update to the latest version, so just click “Update” when it asks you.
If for some reason your Gradle version is not automatically updated, you can manually update it to the latest (2.1.0-alpha4
as of this writing) in your top level build.gradle
:
dependencies {
classpath 'com.android.tools.build:gradle:2.1.0-alpha4'
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
}
Note that this may cause an error saying that you have the wrong Gradle wrapper.
This will go away if you take the error’s suggestion to reimport the project.
Then turn on data binding in your project by adding the following lines to your app/build.gradle
:
android {
compileSdkVersion 21
buildToolsVersion "20.0.0"
...
dataBinding {
enabled = true
}
}
(Feel free to update the build tools and compileSdkVersion
if you like — this is a legacy project, so it has older values out of the box.)
This turns on the build integration for the code generation, but it also turns on Data Binding’s IDE integrations.
To use Data Binding with a specific layout, you have to make a couple of modifications to your code.
The first one will be to your layout.
list_item_crime.xml
is a big-ish RelativeLayout
-based layout file that looks like this (abbreviated for space):
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<CheckBox
android:id="@+id/solved_check_box"
.../>
<TextView
android:id="@+id/title_text_view"
.../>
<TextView
android:id="@+id/date_text_view"
.../>
</RelativeLayout>
At its heart, Data Binding does one big thing:
it takes a layout file (like list_item_crime.xml
) and generates a corresponding Java class, called a binding class.
It will not do this until you tell Data Binding to do its thing, though.
To do that, you wrap your existing layout file in a new tag: <layout>
.
<layout>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
...
</RelativeLayout>
</layout>
When you build and deploy to your device, Data Binding will generate an associated class with a similar name, just in CamelCase: ListItemCrimeBinding
.
Unlike generated code tools like Dagger, Data Binding does not rely on generated code for type checking.
Instead, it is integrated into Android Studio, so that you do not have to wait through a whole code generation pass to use the fields and methods Data Binding provides.
As of this writing, this integration needs a little jump-start to get going.
To make ListItemCrimeBinding
available after adding the <layout>
tag, you must restart Android Studio, then rebuild the project.
Once that is done, you can integrate the binding class into your project.
Here, that will be in your ViewHolder
implementation, CrimeListFragment.CrimeHolder
:
private class CrimeHolder extends RecyclerView.ViewHolder
implements View.OnClickListener {
private final ListItemCrimeBinding mBinding;
private TextView mTitleTextView;
private TextView mDateTextView;
private CheckBox mSolvedCheckBox;
private Crime mCrime;
public CrimeHolder(ListItemCrimeBinding binding) {
super(binding.getRoot());
mBinding = binding;
itemView.setOnClickListener(this);
...
}
Instead of taking in a View
, CrimeHolder
now takes in ListItemCrimeBinding
, which has the View
as its root.
To create the instance of ListItemCrimeBinding
itself, you use DataBindingUtil
instead of directly calling LayoutInflater.inflate
.
In this case, inflation happens inside CrimeListFragment.CrimeAdapter.onCreateViewHolder
.
The Data Binding version of that code looks like this:
private class CrimeAdapter extends RecyclerView.Adapter<CrimeHolder> {
private List<Crime> mCrimes;
public CrimeAdapter(List<Crime> crimes) {
mCrimes = crimes;
}
@Override
public CrimeHolder onCreateViewHolder(ViewGroup parent, int viewType) {
LayoutInflater layoutInflater = LayoutInflater.from(getActivity());
ListItemCrimeBinding binding = DataBindingUtil
.inflate(layoutInflater, R.layout.list_item_crime, parent, false);
return new CrimeHolder(binding);
}
With that, you’re using Data Binding.
Great.
Now what?
The moment you add a <layout>
tag and generate a binding class, Data Binding is doing useful work.
All generated binding classes have a camelCase generated field for each android:id
that is assigned in the layout file.
When the layout is inflated, those views are extracted and bound to their associated fields.
That means that the moment you have an instance of ListItemBinding
, you can immediately ditch all of your findViewById
related code.
All of the fields and findViewById
calls currently in CrimeHolder
can be removed:
private class CrimeHolder extends RecyclerView.ViewHolder
implements View.OnClickListener {
private final ListItemCrimeBinding mBinding;
private Crime mCrime;
public CrimeHolder(ListItemCrimeBinding binding) {
super(binding.getRoot());
mBinding = binding;
itemView.setOnClickListener(this);
}
public void bindCrime(Crime crime) {
mCrime = crime;
mBinding.titleTextView.setText(mCrime.getTitle());
mBinding.dateTextView.setText(mCrime.getDate().toString());
mBinding.solvedCheckBox.setChecked(mCrime.isSolved());
}
Pretty handy.
If you are like me, and you know that this framework generates code, you will want to try and look at the code by doing something like this:
If you do, you may be surprised (like I was).
Instead of taking you to the generated code, it takes you to the layout file.
The reason this happens is the same reason navigating to the declaration of R.layout.list_item_crime
takes you to the layout file instead of the generated R.java
file:
the IDE integration does error checking without the generated code, so that you do not have to rebuild to use a generated field.
If you already know Android Studio inside and out, this is very confusing.
In the long run the IDE integration should have the same benefits that the resource ID integration has.
Note that none of this applies if you’re building outside of Android Studio, where it will behave identically to a tool like Dagger.
It’s also possible to use apt
to skirt around the IDE integration and force the behavior of a typical generated code library.
I don’t recommend it, though — it may feel more familiar initially, but it will be slower in the long run.
Data Binding also gives you the ability to use binding expressions.
Most of this article will be concerned with binding expressions in some way.
The basic idea of a binding expression is small: a binding expression is an expression in an XML attribute that tells your binding class to set a value on a view object.
Binding expressions are very close to Java expressions, plus some additional syntactic sugar, like resource references, to simplify writing view logic.
Binding expressions are particularly handy when you want to assign a value that relies on an existing value in some way, but doesn’t really deserve its own name.
For example, in this version of CriminalIntent, we have the following value assigned to solved_check_box
:
android:padding="@dimen/list_item_padding_2x"/>
As you might imagine, list_item_padding_2x
is twice the value of list_item_padding
.
You can use a binding expression to instead write it this way:
android:padding="@{@dimen/list_item_padding * 2}"/>
Note the special @{}
binding mustache syntax.
This signals that the attribute will be processed by Android Data Binding.
Data Binding will read the expression as it generates the binding class, and generate code to assign this value at runtime.
The XML attribute itself is stripped out of the XML that ships with your app.
Before I continue, I want to mark this down as a potential stopping point for you in your own work.
Admittedly, it’s a bit like heading out on the Oregon Trail from Missouri and stopping in Nebraska.
But there’s a lot to be said for Nebraska, and it’s a long haul to the other side of the Rockies.
So here we are: you can use Data Binding for view binding and basic binding expressions, and stop there.
Any further usage of Data Binding requires a <data>
section (described below).
If you disallow the use of this section in code review, you limit your usage of the tool to these two mechanisms.
If you’re interested in seeing a project at this level of integration, checkout the jingibus/no-data-section
branch of data-binding-talk
.
The biggest reason I like this stopping point is view binding.
View binding dramatically reduces the number of times you have to refer to view names in your code.
Take the name title text view
from above as an example.
Without data binding, you have at least 5 places title text view
must appear in some form:
@+id/title_text_view
in the layout fileR.id.title_text_view
in the call to findViewById
mTitleTextView
in the field definitionmTitleTextView
in the assignment when findViewById
is calledmTitleTextView
when you actually use the widgetButterKnife cuts that down to four:
@+id/title_text_view
in the layout fileR.id.title_text_view
in your @Bind
annotationmTitleTextView
in the field definitionmTitleTextView
when you use the widgetIf you use Data Binding instead, that’s further cut in half:
@+id/title_text_view
in the layout filetitleTextView
when you use the widgetI think that’s a pretty clear win.
The only other tool I know of that competes with Data Binding’s view binding is Kotlin’s Android view extensions,
and that requires you to migrate to Kotlin.
I love Kotlin, but that’s a much bigger change than Data Binding is.
While the layout
tag enrolls you into the cult of Data Binding, delving into its mysteries requires a new element in your XML file: <data>
.
This section goes right after your initial layout
tag:
<layout>
<data>
</data>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
...
In the data
section, you can define variables on the layout.
This gives you the ability to send objects into the layout file.
So you could define a variable for the crime’s title:
<layout>
<data>
<variable
name="crime"
type="com.bignerdranch.android.criminalintent.Crime"/>
</data>
Defining a variable in the data
section creates a property on the associated binding class.
So if you jump back over to CrimeListFragment
, you will see that ListItemCrimeBinding
now has a setCrime
setter:
public void bindCrime(Crime crime) {
mCrime = crime;
mBinding.setCrime(mCrime);
mBinding.titleTextView.setText(mCrime.getTitle());
mBinding.dateTextView.setText(mCrime.getDate().toString());
mBinding.solvedCheckBox.setChecked(mCrime.isSolved());
}
Once you have defined a variable, you can use it within binding expressions.
For example, you could wire up dateTextView
, including a formatting message:
<TextView
android:id="@+id/date_text_view"
android:text="@{`Date discovered: ` + crime.getDate().toString()}"
.../>
(Inside of binding expressions, backticks are interpreted as double quotes. Handy.)
Almost everything that you can write in a Java expression is valid in a data binding expression:
boolean logic, comparisons, ternary logic operators, and so forth.
You cannot use this
, super
, or new
, and you cannot explicitly invoke generics, but everything else is available to you.
Of course, you would not want to use a string literal in your Java code.
You would want to use a formatting string instead.
<resources>
...
<string name="hide_subtitle">Hide Subtitle</string>
<string name="subtitle_format">%1$s crimes</string>
<string name="list_date_format">Date discovered: %1$s</string>
</resources>
You can use formatting strings in binding expressions as functions, passing in the formatting parameters.
<TextView
android:id="@+id/date_text_view"
android:text="@{@string/list_date_format(viewModel.getDate().toString())}"
.../>
Android Studio 2.0 beta 6 also introduced a lambda syntax for hooking up event listeners and other callbacks in binding code.
CrimeHolder
is responsible for handling view clicks.
If you add it as a variable:
<layout>
<data>
<variable
name="crime"
type="com.bignerdranch.android.criminalintent.Crime" />
<variable
name="holder"
type="com.bignerdranch.android.criminalintent.CrimeListFragment.CrimeHolder"/>
</data>
...
And assign it inside of CrimeListFragment.CrimeHolder
:
public CrimeHolder(ListItemCrimeBinding binding) {
super(binding.getRoot());
mBinding = binding;
mBinding.setHolder(this);
}
You can wire up the call to holder.onClick
directly in the layout file.
<layout>
<data>
<variable
name="crime"
type="com.bignerdranch.android.criminalintent.Crime" />
<variable
name="holder"
type="com.bignerdranch.android.criminalintent.CrimeListFragment.CrimeHolder"/>
</data>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:onClick="@{(view) -> holder.onClick(view)}"
android:layout_width="match_parent"
android:layout_height="match_parent">
The expression (view) -> holder.onClick(view)
is a lambda expression.
Data Binding lambda expressions are a limited, abbreviated version of Java 8 lambdas:
no types are permitted in the variable names, and no code blocks are permitted in the body of the lambda.
You can’t leave off the parens, but you can omit the variable name if you aren’t using it.
At runtime, the lambda will behave as if you had defined a complete listener implementation:
new View.OnClickListener() {
@Override
public void onClick(View view) {
holder.onClick(view);
}
};
So how does it work?
When are binding expressions run?
How they triggered?
I could talk about it, but I’d rather show you.
As mentioned before, Data Binding is integrated into the IDE, so it does not always generate code.
You can force code to be generated, though.
Run your project, and if your build and deploy is successful, you should have generated code.
Switch to the Project
view in your Project
tab in Android Studio.
The generated code goes in app/build/intermediates/classes/debug
(or classes/release
, for a release build).
Deep inside classes/debug
, in com/bignerdranch/android/criminalintent/databinding
, you will find the generated ListItemCrimeBinding.java
class.
Double click to crack it open.
We know that the views must be repopulated when you call setCrime
.
Here’s what setCrime
looks like as of this writing (no guarantees it will stay the same):
public void setCrime(com.bignerdranch.android.criminalintent.Crime crime) {
this.mCrime = crime;
synchronized(this) {
mDirtyFlags |= 0x2L;
}
super.requestRebind();
}
Each individual source value that can be displayed has its own dirty bit in mDirtyFlags
.
When that source value changes, the dirty bit is flipped and a rebind is requested.
What does it mean to request a rebind? Well, here’s what requestRebind()
looks like:
protected void requestRebind() {
synchronized (this) {
if (mPendingRebind) {
return;
}
mPendingRebind = true;
}
if (USE_CHOREOGRAPHER) {
mChoreographer.postFrameCallback(mFrameCallback);
} else {
mUIThreadHandler.post(mRebindRunnable);
}
}
Requesting a rebind simply posts a callback to be run on the main thread as soon as possible.
So you can assign new values to three different variables, and it will only run the bindings one time.
(The actual method on ListItemCrimeBinding
that assigns the values is called executeBindings()
. We’ll check that out in a minute.)
This is a problem in a RecyclerView
, though.
A RecyclerView
could be scrolling very quickly.
If rebinding does not happen immediately, there is a possibility of visible flicker.
There is a straightforward fix, thankfully.
If you need the bindings executed immediately, just call executePendingBindings()
on your binding class.
public void bindCrime(Crime crime) {
mCrime = crime;
mBinding.setCrime(crime);
mBinding.executePendingBindings();
}
This will run the code for all of your binding expressions, and unset the dirty bits for all of the variables you changed.
As was mentioned above, binding expressions are not only Java expressions.
They’re intended to write small bits of code to wire up views.
To make that easier, binding expressions have some additional syntax and semantics differences that make it easier to write brief view logic.
The first semantic difference is that binding expressions generate code that treats null
differently the regular Java code.
If you dive into ListItemCrimeBindings.executeBindings()
, you will find a lot of code like this:
if (crime != null) {
// read crime~~getDate~crime~
crimeGetDateCrime = crime.getDate();
}
if (crimeGetDateCrime != null) {
// read crime~~getDate~crime~~toString~crime~~getDate~crime~
crimeGetDateCrimeToS = crimeGetDateCrime.toString();
}
No methods are ever called without first verifying that the method’s recipient is not null.
The effect of this is that null
valued objects behave as if they receive messages, like in Obj-C.
Method invocations on null
run no code, and always yield default values.
This preserves some existing Java behavior, while making it more resilient to the presence of null
.
Take the example from earlier, where you used string concatenation:
"@{`Date discovered: ` + crime.getDate().toString()}"
If crime
or crime.getDate()
is null
here, the entire crime.getDate().toString()
expression ends up being null
.
Since Java string concatenation converts null
values to the string "null"
,
the whole expression evaluates to "Date discovered: null
.
This behavior prevents many NullPointerException
s, but not all.
null
receivers may send null
values on to other methods.
For example, if you wrote the following:
"Date discovered: ".concat(crime.getDate().toString())
concat
throws a NullPointerException
if it receives a null
parameter.
So you’d get an NPE if crime
were null.
Another way binding expressions help abbreviate null handling is through the null coalescing operator, ??
.
The null coalescing operator helps when you want to provide a default value when some other value is null.
Take this for example:
"@{@string/list_date_format(crime.getDate().toString() ?? `(no date)`)}"
Instead of showing "Date discovered: null"
for a null value, you would see "Date discovered: (no date)"
.
You also don’t have to write out property getters.
So instead of:
"@{@string/list_date_format(crime.getDate().toString() ?? `(no date)`)}"
You can write:
"@{@string/list_date_format(crime.date.toString() ?? `(no date)`)}"
This works for other standard getter conventions like isSolved()
, too.
There are a couple of gotchas to keep in mind with Data Binding right now.
How temporary these are is unknown, but for now they are good to know about.
(They shed some light on things worth knowing, too.)
Remember that earlier I said that setting a binding expression on an attribute causes that attribute to be processed by Data Binding, not by the XML layout inflation process.
And as we have seen, binding expressions are all about setting values on view objects.
What happens when you set a binding expression on a layout parameter, though?
You might want to do the same trick with margins that you used on padding:
...
<TextView
android:id="@+id/date_text_view"
android:text="@{@string/list_date_format(crime.date.toString() ?? `(no date)`)}"
android:layout_margin="@{@dimen/list_item_padding * 2}"
.../>
...
This will throw an error:
~/src/android/CriminalIntent/app/src/main/res/layout/list_item_crime.xml
Error:(38, 38) Cannot find the setter for attribute 'android:layout_margin'
with parameter type float.
This is because, as of right now, data binding is a mechanism for setting values on view objects.
That means that under the hood, all of your attribute setting is being converted to method calls.
Which is what the error is complaining about: it cannot find a setter for a property named layout_margin
.
More about that in a moment.
In current versions of Data Binding, XML errors can be confusing.
For example, if you wrote the following in your XML file:
...
<TextView
android:id="@+id/date_text_view"
android:text="@{@string/list_date_format(crime.date.toString() ?? `(no date)`)}"
android:allCaps="true"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_toLeftOf="@id/solved_check_box"
android:layout_below="@id/title_text_view"
tools:text="Crime Date"
android:padding="@{@dimen/list_item_padding * 2}"/>
...
The attribute android:allCaps
does not exist — it’s actually android:textAllCaps
.
If you try to run the app, you get the following error:
~/src/android/CriminalIntent/app/build/intermediates/data-binding-layout-out/debug/layout/list_item_crime.xml
Error:(35) No resource identifier found for attribute 'allCaps' in package 'android'
So you say, “D’oh,” as is the fashion at this time, and double click on the error to go fix it.
That will pop you into this code:
...
<TextView
android:id="@+id/date_text_view"
android:tag="binding_3"
android:allCaps="true"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_toLeftOf="@id/solved_check_box"
android:layout_below="@id/title_text_view"
tools:text="Crime Date"
...
At first glance, this looks like the same place you just were.
You might even fix the offending line here and rebuild.
It won’t work though, because this is actually a post-processing version of the file.
Remember above when I said that XML attributes are stripped out to be processed by the data binding tool?
If you look closely, you will see that this listing is actually missing two attributes:
android:text
and android:padding
.
This is your layout XML after those attributes are stripped.
Those attributes were assigned with binding expressions, and so the code for them is in ListItemCrimeBinding
.
If you run into this problem, it means that you will have to manually reopen the layout file in res/layout
.
If you do not, you will get into a loop where you think you have fixed the problem, and then find the error you fixed pops up again immediately.
I have found myself going through a couple of cycles of this, even though I know about it.
But hey — at least I know.
Strangely enough, you actually can get that non-existent android:allCaps
attribute to work.
All you have to do is change its assignment to use a binding expression.
...
<TextView
android:id="@+id/date_text_view"
android:text="@{`Date discovered: ` + crime.date.toString() ?? `(no date)`}"
android:allCaps="@{true}"
.../>
...
Remember that a binding expression says, “Data Binding, please process this attribute assignment for me.”
The default way Data Binding processes an attribute is to look for a single-parameter setter on the view with the same type as the binding expression.
And, as it turns out, the setter associated with the android:textAllCaps
attribute is called setAllCaps(boolean)
.
So android:allCaps
works perfectly fine.
This is what’s called an automatic setter.
If a setter exists on a view, you can always set it by using a binding expression with the appropriate type.
Some widget properties you would want to assign aren’t single-parameter setters, though.
And some attributes, like android:textAllCaps
, don’t have the same name as their setters.
For example, say you had an EditText
that you wanted to setup a text listener for.
There is no documented way to configure this interface in XML.
In Java, you would call addTextChangedListener
with an implementation of the TextWatcher
interface:
public interface TextWatcher extends NoCopySpan {
void beforeTextChanged(CharSequence var1, int var2, int var3, int var4);
void onTextChanged(CharSequence var1, int var2, int var3, int var4);
void afterTextChanged(Editable var1);
}
Can you use it with Data Binding?
If so, how do you find out how it works?
There is not yet any official documentation, but there is still a way to find these attributes.
All custom attribute behavior is implemented through binding adapters.
If you need to know what custom attributes apply to a kind of widget, all you need to do is find its binding adapters and you will see all the custom attributes.
The binding adapters are written in a class with the widget’s class name, plus Adapter
on the end.
So for TextView
, you can find the binding adapters by going to Navigate->Open class in Android Studio (or Cmd-O, if you’re using the same keybindings as me).
Type in TextViewBindingAdapter
, and open up the class that it finds.
At the top, you’ll initially see this:
@BindingMethods({
@BindingMethod(type = TextView.class,
attribute = "android:autoLink",
method = "setAutoLinkMask"),
...
@BindingMethod(type = TextView.class,
attribute = "android:onEditorAction",
method = "setOnEditorActionListener"),
})
public class TextViewBindingAdapter {
...
For attributes like android:textAllCaps
, where the method name does not match up with the attribute name, a BindingMethod
attribute points Data Binding in the right direction.
This isn’t the case for TextWatcher
, so you will not find what you need in this section.
Type Cmd-F to do a search in this file, and look for TextWatcher
.
You should jump right to this method:
...
@BindingAdapter(value = {"android:beforeTextChanged",
"android:onTextChanged",
"android:afterTextChanged"},
requireAll = false)
public static void setTextWatcher(TextView view, final BeforeTextChanged before,
final OnTextChanged on, final AfterTextChanged after) {
...
Paydirt.
This is what a binding adapter implementation looks like.
It is an annotated static method, which may handle one or more attributes.
The first parameter of the method is always the kind of view the binding adapter applies to.
Following that is one parameter for each attribute handled by the adapter.
This adapter handles three attributes, one for each method on the TextWatcher
interface.
The parameters correspond, in order, to the list of attribute names specified in the @BindingAdapter
annotation’s value
parameter.
Now you know how to configure a TextWatcher
callback.
OnTextChanged
has one method, onTextChanged
, which takes in four parameters: a String
, and three int
s.
public interface OnTextChanged {
void onTextChanged(CharSequence s, int start, int before, int count);
}
So to listen to TextWatcher.onTextChanged()
, you would write the following binding expression in your layout file:
android:onTextChanged="@{(s, start, before, count) -> holder.onTitleTextChanged()}"
Lambdas let you omit the callback parameters, too, if you aren’t using them.
So you could also write your callback like so:
android:onTextChanged="@{() -> holder.onTitleTextChanged()}"
If you need an attribute that is not provided, you can write your own binding adapters.
Complete coverage of that topic is a little beyond the scope of this article, but the source for TextViewBindingAdapter
above is a great place to start.
The Data Binding Guide also explains this topic in some detail.
I’ve shown you how to use Data Binding to pull values from model objects into your layout automagically.
Unfortunately, I’ve also made a mess: my layout is integrating code from a few different areas.
That puts a lot of real responsibility in the layout file.
That’s a bad thing.
Responsibility means you need to provide oversight.
Oversight means code review and tests.
You don’t want to code review your layout files in this way — the functionality will disappear amongst all the display concerns.
And you definitely don’t want to put it under test.
So the last bit of cleanup work to do here is to take the interaction and data formatting pieces out of the layout file and into a new class that will take on that responsibility: a view model.
I create my class, and call it CrimeListItemViewModel
, since its responsibility is to the crime list item.
I add a couple of dependencies:
public class CrimeListItemViewModel {
private final Context mContext;
private Crime mCrime;
public CrimeListItemViewModel(Context context) {
mContext = context.getApplicationContext();
}
public Crime getCrime() {
return mCrime;
}
public void setCrime(Crime crime) {
mCrime = crime;
}
}
This view model is showing information about a Crime
, so it definitely needs a Crime
.
It will start a new activity when it is selected, too, so it needs a Context
.
Then you expose getters and event trigger methods:
...
public CrimeListItemViewModel(Context context) {
mContext = context;
}
public Crime getCrime() {
return mCrime;
}
public void setCrime(Crime crime) {
mCrime = crime;
}
public String getTitle() {
return mCrime.getTitle();
}
public String getRenderedDate() {
return mCrime.getDate().toString();
}
public boolean isSolved() {
return mCrime.isSolved();
}
public void onCrimeSelected() {
Intent intent = CrimePagerActivity.newIntent(mContext, mCrime.getId());
mContext.startActivity(intent);
}
}
Update the layout file to use an instance of CrimeListItemViewModel
instead of Crime
and CrimeHolder
.
<?xml version="1.0" encoding="utf-8"?>
<layout>
<data>
<variable
name="viewModel"
type="com.bignerdranch.android.criminalintent.CrimeListItemViewModel" />
</data>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:onClick="@{() -> viewModel.onCrimeSelected()}"
...>
<CheckBox
android:id="@+id/solved_check_box"
android:checked="@{viewModel.solved}"
.../>
<TextView
android:id="@+id/title_text_view"
android:text="@{viewModel.title}"
.../>
<TextView
android:id="@+id/date_text_view"
android:text="@{`Date discovered: ` + viewModel.renderedDate ?? `(no date)`}"
...
android:padding="@{@dimen/list_item_padding * 2}"/>
</RelativeLayout>
</layout>
Then change your CrimeHolder
to use the view model, too.
public class CrimeHolder extends RecyclerView.ViewHolder {
private final ListItemCrimeBinding mBinding;
private final CrimeListItemViewModel mViewModel;
public CrimeHolder(ListItemCrimeBinding binding) {
super(binding.getRoot());
mBinding = binding;
mViewModel = new CrimeListItemViewModel(getActivity());
mBinding.setViewModel(mViewModel);
}
public void bindCrime(Crime crime) {
mViewModel.setCrime(crime);
mBinding.executePendingBindings();
}
}
This idea of using a view model class is often called “model-view-viewmodel”, or MVVM.
This is not a complete coverage of the idea of MVVM architecture.
But it does show the essentials of the idea.
Look great, right?
Not if you scroll down, it’s not:
The list items are never getting updated with the new information being sent to ListItemCrimeViewModel
.
And how could they? The binding object doesn’t know.
One fix is to add a call to ListItemCrimeBinding.invalidateAll()
, which will trigger a rebind of everything.
Far less brittle, though, is to use an observable object instead.
A observable Data Binding object implements the android.databinding.Observable
interface.
This is a different from RxJava’s Observable
interface: a Data Binding Observable
exposes the ability to listen to changes on an individual properties of an object.
Implementing Observable
requires writing plumbing to hook up listeners to each individual property.
Rather than writing this yourself, you are almost always better off extending BaseObservable
instead:
public class CrimeListItemViewModel extends BaseObservable {
...
You then need to specify which properties of your class may be bound to by annotating them with @Bindable
:
...
@Bindable
public String getTitle() {
return mCrime.getTitle();
}
@Bindable
public String getRenderedDate() {
return mCrime.getDate().toString();
}
@Bindable
public boolean isSolved() {
return mCrime.isSolved();
}
...
The last step is to notify whoever is observing your object when one of these properties changes.
Typically, this is done by calling notifyPropertyChanged(int)
.
The int
is a special constant for each property name in a file called BR.java
.
BR.java
is a generated file similar to R.java
.
Instead of containing resource IDs, BR.java
contains binding resource IDs — integer constants that can be used to identify properties by name instead of String
s.
When you mark a field or getter with @Bindable
, a matching constant with the same name is added to BR.java
.
So to say that getTitle()
’s value has changed, you would call:
notifyPropertyChanged(BR.title);
In CrimeListViewModel
, the only thing that triggers property changes is a call to setCrime(Crime)
, and that triggers changes to all the properties.
Which you can signify by just calling notifyChange()
:
public void setCrime(Crime crime) {
mCrime = crime;
notifyChange();
}
This signals that all of the object’s properties have changed.
It was quite a lot of ground to cover, but now you’re at the other end of the trail.
(If you check out the code from the repo, this is what you see on the master
branch.)
If you want to use Data Binding to its fullest potential, I recommend going all the way here.
Stopping without going all the way to a View Model architecture is like stopping in east Oregon — Portland is almost there!
And there are food trucks there!
Seriously, though, there are some very nice things about this architecture, particularly if you’re doing testing.
RelativeLayout
, it would not need any ids at all.Not everyone will find it worthwhile to make the migration to this full-blown usage of data binding.
If you don’t, I recommend stopping at the simple usage described above in the “Simple data binding” section.
Life is easier without them, but I have a hard time writing conclusions without sharing an opinion.
So here’s some short and sweet spit takes:
My first one might be a bit controversial: if you’re using apt
with Data Binding, I recommend stopping.
It’s only slowing you down.
Relying on the IDE integration will make it easier for you to navigate your codebase, and eliminate the need to rebuild to make fields visible.
If you can pair MVVM with Binding, I recommend doing so.
We’ve already used it in some production apps, to good effect.
The initial hurdles can be a little painful, but it really kills a lot of annoying boilerplate in your Java code.
If you cannot use a View Model architecture, I still recommend using Data Binding, but only as a view binding tool.
Without MVVM or a similar discipline governing what kinds of objects you use in your layout file,
your layout files can easily become a locus of maintenance nightmares.
Debugging is hard enough without having to add your resources folder to the places you need to hunt down business logic.
So don’t go halfsies on this.
There may be other sweet spots for Data Binding usage.
MVVM and view binding are the two I can recommend today, though.
And I think that’s about all I have to say about Data Binding.
Introduction When computer programming was young, code was punched into cards. That is, holes were punched into a piece of cardboard in a format...
Jetpack Compose is a declarative framework for building native Android UI recommended by Google. To simplify and accelerate UI development, the framework turns the...
Big Nerd Ranch is chock-full of incredibly talented people. Today, we’re starting a series, Tell Our BNR Story, where folks within our industry share...