If you have written any Android code at all, you have written this line:
TextView textQuestion = findViewById(R.id.textQuestion);And if you have written it twenty times in one Activity, you have probably wondered whether there is a better way. There is. It is called data binding, and this article walks through it from nothing to a working quiz app.
findViewById has three annoyances that grow with the size of your app.
It is repetitive. Every widget you want to touch needs its own lookup line, its own variable, and its own cast.
It fails at runtime, not compile time. Type R.id.textQuestoin and the code compiles happily. The app then crashes when it runs, because the lookup returned null and you called a method on it.
It is verbose. Six widgets means six lines of setup before you write a single line of logic.
Data binding replaces all of that with one object that already holds every widget in the layout.
findViewById is looking up a phone number in a directory every single time you want to call someone. Data binding is having the whole contact list already on your phone — you just tap the name.Without data binding:
TextView textQuestion = findViewById(R.id.textQuestion);
Button buttonTrue = findViewById(R.id.buttonTrue);
Button buttonFalse = findViewById(R.id.buttonFalse);
textQuestion.setText("Kuala Lumpur is the capital of Malaysia.");With data binding:
binding.textQuestion.setText("Kuala Lumpur is the capital of Malaysia.");No lookups. And crucially, if you mistype binding.textQuestoin, the project will not compile — you find out immediately instead of when the app crashes on a user's phone.
There are three steps, and all three are required. Miss one and the binding class never appears.
Open app/build.gradle and add the buildFeatures block inside android { }:
android {
// ... existing config ...
buildFeatures {
dataBinding true
}
}Then click Sync Now in the bar that appears at the top of the editor. Nothing works until you sync.
<layout>Data binding only generates a class for layouts whose root element is <layout>. This is the step people forget.
Before:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<TextView android:id="@+id/textQuestion" ... />
</LinearLayout>After:
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<TextView android:id="@+id/textQuestion" ... />
</LinearLayout>
</layout>Note that the xmlns:android declaration moves up to the <layout> tag — it belongs on the root element, whichever that is.
Replace setContentView() with DataBindingUtil.setContentView():
public class MainActivity extends AppCompatActivity {
private ActivityMainBinding binding;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
binding = DataBindingUtil.setContentView(this, R.layout.activity_main);
binding.textQuestion.setText("Ready?");
}
}From this point on, every widget with an android:id is reachable through binding.
ActivityMainBinding come from?You never write this class. The build generates it from your layout file name, converted to PascalCase with Binding appended.
| Layout file | Generated class |
|---|---|
activity_main.xml | ActivityMainBinding |
activity_result.xml | ActivityResultBinding |
fragment_profile.xml | FragmentProfileBinding |
list_item_question.xml | ListItemQuestionBinding |
If the class does not exist, the cause is almost always one of the three setup steps above — or you simply need to rebuild.
The IDs in your XML become fields on the binding object, converted from snake_case to camelCase:
| In XML | In Java |
|---|---|
android:id="@+id/text_question" | binding.textQuestion |
android:id="@+id/button_true" | binding.buttonTrue |
android:id="@+id/image_result" | binding.imageResult |
A widget with no android:id does not appear on the binding object at all. If a field is missing, check that the widget has an id.
Everything you already know still applies — only the way you reach the widget changes.
// WRITE — change what the user sees
binding.textQuestion.setText("Kuala Lumpur is the capital.");
binding.imageResult.setImageResource(R.drawable.ic_correct);
binding.buttonNext.setEnabled(false);
// READ — get what the user typed
String answer = binding.editAnswer.getText().toString();
// LISTEN — react to a tap
binding.buttonTrue.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
checkAnswer(true);
}
});Let us build something real. The app shows a statement, the user answers True or False, and Next moves to the following question.
A plain Java class holding one question:
public class Question {
private String text;
private boolean answerTrue;
public Question(String text, boolean answerTrue) {
this.text = text;
this.answerTrue = answerTrue;
}
public String getText() { return text; }
public boolean isAnswerTrue() { return answerTrue; }
}private Question[] questionBank = new Question[] {
new Question("Kuala Lumpur is the capital of Malaysia.", true),
new Question("Malaysia has 12 states.", false),
new Question("Mount Kinabalu is in Sabah.", true),
};
private int currentIndex = 0;private void updateQuestion() {
binding.textQuestion.setText(questionBank[currentIndex].getText());
}private void checkAnswer(boolean userPressedTrue) {
boolean correct = questionBank[currentIndex].isAnswerTrue();
String message = (userPressedTrue == correct)
? "Correct!"
: "Not quite.";
Snackbar.make(binding.getRoot(), message, Snackbar.LENGTH_SHORT).show();
}binding.getRoot() is the whole layout — a Snackbar needs a view to attach to, and that is the easiest one to hand it.
binding.buttonTrue.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
checkAnswer(true);
}
});
binding.buttonFalse.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
checkAnswer(false);
}
});
binding.buttonNext.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
currentIndex = (currentIndex + 1) % questionBank.length;
updateQuestion();
}
});That % (modulo) is what makes the quiz wrap around instead of crashing at the end of the array.
<data> blockEverything above uses data binding's binding object and nothing else. That is perfectly valid, and it is where most people start.
But data binding has a second half: binding expressions. You declare a variable inside a <data> block and reference it directly in the XML:
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<variable
name="question"
type="com.example.truecitizen.Question" />
</data>
<LinearLayout ... >
<TextView
android:id="@+id/textQuestion"
android:text="@{question.text}" />
</LinearLayout>
</layout>Then in Java you hand the object over and the layout updates itself:
binding.setQuestion(questionBank[currentIndex]);No setText() call at all. The @{...} syntax reads the property straight off your model.
The <data> block is optional — omit it and everything in this article still works. It is worth knowing about because it is what distinguishes data binding from the lighter-weight view binding, which offers the binding object but no expressions.
| What you see | What went wrong | Fix |
|---|---|---|
Cannot resolve symbol ActivityMainBinding | Not enabled, or not synced | Add dataBinding true, then Sync Now |
| Binding class still not found | Layout root is not <layout> | Wrap it, then Rebuild Project |
A widget is missing from binding | No android:id on it | Give the widget an id |
NullPointerException on binding | Used before onCreate assigned it | Assign it first thing in onCreate |
| Changes to XML not showing up | Stale generated code | Build → Rebuild Project |
The golden rule: when something about data binding looks impossible, rebuild the project before you debug anything else. The binding classes are generated at build time, and a stale build is the single most common cause of confusion.
dataBinding true in app/build.gradle, then Sync Now<layout>DataBindingUtil.setContentView() in onCreateactivity_main.xml generates ActivityMainBindingsnake_case ids become camelCase properties<data> block and @{} expressions are optional extras