Add Data Binding to the App

Kotlin and Android Development featuring Jetpack — by Michael Fazio (19 / 125)

The Pragmatic Programmers
The Pragmatic Programmers
10 min readSep 23, 2021

--

👈 Build a Fragment (Pick Players) | TOC | Build Another Fragment (G ame) 👉

Here, we’re going to turn on data binding for our app and add some configuration for each player item. In my opinion, data binding is one of the best and most useful parts of Jetpack. It allows us to take explicit property binding out of our Activity and Fragment classes and put it into the layout XML. We can then use these bindings to set values, hide/show views, and even create forms like the Players form here in PickPlayersFragment. We’ll be able to send the variables from fragment_pick_players.xml into each instance of player_list_item.xml and use those variables to conditionally display pieces of the list items. But first, we need to turn the feature on for our project.

Enable Data Binding

This will be a two-step process in the app’s build.gradle file: add the Kotlin kapt plugin and add the dataBinding flag. kapt is the Kotlin-specific annotation processing plugin, which allows us to use (among other things) data binding in the app.

​ apply plugin: ​'com.android.application'​

​ apply plugin: ​'kotlin-android'​

»apply plugin: ​'kotlin-kapt'​


​ android {
» buildFeatures {
» dataBinding = ​true​
» }

​ Rest of the android block is down here.
​ }

​ ​// Dependencies live down here​

With both those changes made and following a Gradle sync, we can move on to layout_pick_players.xml, where we’ll wrap our <ConstraintLayout> in a generic <layout> tag.

Add a Generic layout Tag to fragment_pick_players.xml

To use data binding in a view component, we need to wrap the entire view in a generic <layout> tag. This tag won’t change the look and feel of the view, but it will cause a class to be generated to allow bindings to occur. We move the xmlns:* declarations to that generic <layout> tag and fill in our main view like normal:

​ ​<?xml version="1.0" encoding="utf-8"?>​
​ <layout 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"​>

​ <androidx.constraintlayout.widget.ConstraintLayout
​ android:layout_width=​"match_parent"​
​ android:layout_height=​"match_parent"​
​ android:layout_margin=​"16dp"​>

​ ​<!-- All the inner views stay the same -->​

​ </androidx.constraintlayout.widget.ConstraintLayout>
​ </layout>

If we run the app after this change, it’ll look exactly the same as it did before. But we can now update the PickPlayersFragment to use the FragmentPickPlayersBinding class that was generated for us, start adding binding statements to our views, and reference one or more <variable> tags in a layout. We’ll start by making the changes to PickPlayersFragment, add some <variable> tags over in player_list_item.xml, then finally come back to layout_pick_players.xml to send in the values.

Update PickPlayersFragment

Right now, the onCreateView inside PickPlayersFragment should look something like this:

​ ​override​ ​fun​ ​onCreateView​(
​ inflater: LayoutInflater, container: ViewGroup?,
​ savedInstanceState: Bundle?
​ ): View? {
​ ​// Inflate the layout for this fragment​
​ ​return​ inflater.inflate(
​ R.layout.fragment_pick_players,
​ container,
​ ​false​
​ )
​ }

We now need to change some things up to use data binding for the view, including how we inflate the view.

Use Data Binding in PickPlayersFragment

We’ve already turned on data binding for PickPlayersFragment when we added the generic <layout> inside fragment_pick_players.xml, so now we just have to use it inside our code. We’re going to use the generated FragmentPickPlayersBinding class to inflate our view with the additional binding components.

​ override fun onCreateView(
​ inflater: LayoutInflater, container: ViewGroup?,
​ savedInstanceState: Bundle?
​ ): View? {
​ val binding =
​ FragmentPickPlayersBinding.inflate(inflater, container, false)

​ return binding.root
​ }

Once our view is inflated with the FragmentPickPlayersBinding class, we can directly access components via ID instead of having to call findViewById. As you’ll see soon, this is a much better approach than the “old” way of handling things.

We’ll do a lot more with the binding object once we get to our ViewModel classes. Also, we’re going to stick with the two-line version of onCreateView even though we could turn this into a single-expression method. This is because we’re going to be binding components in there soon.

Now, if we weren’t using the Data Binding library, the inside of onCreateView would look more like the upcoming code block. We’d spend time in here looking up different views by ID, then setting their properties like this:

​ ​val​ view = inflater.inflate(
​ R.layout.fragment_pick_players,
​ container,
​ ​false​
​ )
»with(view?.findViewById<View>(R.id.mainPlayer)) {
» ​this​?.findViewById<CheckBox>(R.id.checkbox_player_active)?.let {
» it.isVisible = ​false​
» it.isChecked = ​true​
» }
»}

​ ​return​ view

Wait, What Was That Code Block?

Even though we’re not using that code block, it could use some explanation, as it probably looks crazy to anyone just starting out with Kotlin. The high-level idea here is that we grab the row that we want, then from there get the <CheckBox> inside. Once we have the <CheckBox>, we can set a couple of properties. This is actually the same thing we’re going to do in a bit in our XML view.

The first block is a with block, which takes whatever is in the parentheses and converts that to the keyword this. with is one of the many scope functions in Kotlin, where we execute a block of code using the context of a given object. I chose with here to illustrate its use, plus it made logical sense to me to look at the function as “With the mainPlayer view, find the active player checklist.” You could use other scope functions here instead if you prefer.

In the case of this example, we have an expression that uses the main view of the Fragment and searches for another view with the ID mainPlayer. The result of that search is passed into the with block. The last item of note on this line is the ?, which is the null-safe operator.

This causes the code to only run the subsequent expression(s) if the preceding expression is not null. In this case, if view is null, we never try to call findViewById, and so we avoid the NullPointerException. This is similar to calling if (view != null) { … } around this line, but with less code.

Inside the with block, we’re doing something similar. If the result was null, we skip the second look up by ID. If not, then we attempt to find the CheckBox with the ID checkbox_player_active. We then do one more null-safe check, but we use the let function this time. let is another scope function (like with) that is especially useful for situations like this one, as we can use ? to only execute the block if the expression coming into let is not null. Once inside the let block, we can refer to the CheckBox with the implicit variable it. Kotlin automatically creates it for us inside any block where we haven’t explicitly named the parameters coming in. For example, we could give the CheckBox view a name and use this code instead:

​ ​this​?.findViewById<CheckBox>(R.id.checkbox_player_active)?.let { checkBox ->
​ checkBox.isVisible = ​false​
​ checkBox.isChecked = ​true​
​ }

Hopefully this block is clearer to you, as the Kotlin-specific syntax will definitely be useful down the road. But as far as the PickPlayersFragment is concerned, we’re going to stick with the data-binding approach.

Update the Pick Players Layout

Now that our layout file has a generic <layout> tag and we’ve changed our PickPlayersFragment to use FragmentPickPlayersBinding, we can add in a <data> block with one or more <variable> tags. The <data> tag gives us a place to import classes or declare variables, while the <variable> tag lets us list those variables for use within our views.

In this case, we’re going to start with hiding the <CheckBox> and <SwitchCompat> views for a couple of rows, then leaving them in for the rest. For the first player, we know our game must have at least two players and one of them has to be a human player. So we can hide both the <CheckBox> and <SwitchCompat> on the first row. The second player must also be included (meaning the <CheckBox> is hidden again), but we want to leave the <SwitchCompat> in case someone wants to play against a single AI player.

We can update the attributes in player_list_item.xml, then send the values in from fragment_pick_players.xml once that’s done. Keep in mind that there’s nothing more we have to do right now inside PickPlayersFragment to get these pages to work (though we’ll be making changes there later). Let’s start with the <data> and <variable> tags inside player_list_item.xml, then we can set the values for each view.

Add data and variable Tags Inside player_list_item.xml

Just as we did in fragment_pick_players.xml, we’re going to add a generic <layout> tag around our existing <ConstraintLayout> to allow for data binding. Once we add that <layout> tag, we can add a <data> tag along with two <variable> tags: checkoutHidden and switchHidden. Both variables are Boolean types, which we’ll declare within the <variable> tags.

​ <layout xmlns:android=​"http://schemas.android.com/apk/res/android"​
​ xmlns:app=​"http://schemas.android.com/apk/res-auto"​>

​ <data>
​ <variable
​ name=​"checkboxHidden"​
​ type=​"Boolean"​ />

​ <variable
​ name=​"switchHidden"​
​ type=​"Boolean"​ />
​ </data>
​ ​<!-- The rest of our view lives down here. -->​
​ </layout>

That’s all we have to do to configure the variables, so we can start using them in our view. First stop, the <Checkbox> tag.

Conditionally Display the CheckBox for a Player

Now, we want to conditionally set the visibility and checked attributes for our <CheckBox>. We do that by using the special data-binding syntax, which looks something like this:

​ android:checked="@{checkboxHidden}"

By putting the variable inside the brackets with an @ in front, the Data Binding library knows that this is a data-binding expression and it will use the variables we declared in the <data> section. So whatever value is set to checkboxHidden is used to set a default value for android:checked.

I say “default value” here because it’s only the value that’s used when the view is created, rather than updating every time the android:checked value changes. This is important because if the value of checkboxHidden would change when clicking a <CheckBox>, it would also hide our <CheckBox> at the same time. We are going to work with binding in this fashion, called two-way binding, in a little while but not quite yet. Now, we need to change the android:visibility based on what’s in checkboxHidden.

It may seem odd that I started with android:checked when the focus here is hiding the <CheckBox>, but I wanted to show a simpler example first where we just assign a variable before getting any more complicated. The Data Binding library allows us to do quite a bit inside a binding expression, as the library’s expression language contains a ton of operators/keywords, from mathematical symbols to logical comparisons to method calls.

In our case, we’re going to add a ternary operator ([conditional] ? [if true] : [else]) to change the android:visibility based on the value inside checkboxHidden. For the android:visibility attribute, we’re going to switch between two states: VISIBLE and INVISIBLE. A third state, GONE, is also available, but that completely removes the view from its parent. In our case, this would cause the Player Name text field to expand to the left side of the view instead of staying the same size as the other rows. The end result for android:visibility will look like this:

​ android:visibility="@{checkboxHidden ? View.INVISIBLE : View.VISIBLE}"

We need to prepend both visibility states with View since they’re actually constant int values on that class. The slight issue with that is the fact our view doesn’t know about the View class, but we can quickly fix that inside our <data> section. Add an <import> tag with the type attribute containing the (fully-qualified) name of the class you want to include. In our case, it’s android.view.View.

​ <data>
» <import type=​"android.view.View"​ />

​ <variable
​ name=​"checkboxHidden"​
​ type=​"Boolean"​ />

​ <variable
​ name=​"switchHidden"​
​ type=​"Boolean"​ />
​ </data>

Now our <CheckBox> is ready to go, at least inside player_list_item.xml. The <SwitchCompat> is going to be the same idea but using switchHidden instead:

​ android:visibility="@{switchHidden ? View.INVISIBLE : View.VISIBLE}"

Kotlin Has No Concept of a Ternary Statement

WARNING

I know, we appeared to have just used them with android:visibility, but that was the data-binding expression language rather than true Kotlin code. Instead of using ternary statements, we’ll use if…else expressions in our Kotlin code. Since if..else blocks are expressions in Kotlin (meaning they return a value), they can be assigned to values or directly returned from methods. We’ll look at this more in Chapter 4, Update LiveData with Conditional Game Logic.

We’re done with player_list_item.xml, so let’s move back to pick_players_fragment.xml.

Assign Values to Variables

Back in pick_players_fragment.xml, we’re able to assign values to the two variables in each item. By referencing the app namespace, we can assign the variables as we would any other attributes. This means we have access to both app:checkboxHidden and app:switchHidden and can use the same binding expresions we saw earlier to set these values:

​ <include
​ android:id=​"@+id/mainPlayer"​
​ layout=​"@layout/player_list_item"​
​ app:checkboxHidden=​"@{true}"​
​ app:switchHidden=​"@{true}"​ />

By setting the values here, only the mainPlayer player item will hide the <CheckBox> and <SwitchCompat> and the rest will remain as is. Remember that we also want to hide the <CheckBox> for player two in the same manner. Taking both sets of changes will turn the <LinearLayout> into this:

​ <LinearLayout
​ android:layout_width=​"match_parent"​
​ android:layout_height=​"wrap_content"​
​ android:orientation=​"vertical"​
​ app:layout_constraintEnd_toEndOf=​"parent"​
​ app:layout_constraintStart_toStartOf=​"parent"​
​ app:layout_constraintTop_toTopOf=​"parent"​>

» <include
» android:id=​"@+id/mainPlayer"​
» layout=​"@layout/player_list_item"​
» app:checkboxHidden=​"@{true}"​
» app:switchHidden=​"@{true}"​ />

» <include
» android:id=​"@+id/player2"​
» layout=​"@layout/player_list_item"​
» app:checkboxHidden=​"@{true}"​ />

​ <include
​ android:id=​"@+id/player3"​
​ layout=​"@layout/player_list_item"​ />

​ <include
​ android:id=​"@+id/player4"​
​ layout=​"@layout/player_list_item"​ />

​ <include
​ android:id=​"@+id/player5"​
​ layout=​"@layout/player_list_item"​ />

​ <include
​ android:id=​"@+id/player6"​
​ layout=​"@layout/player_list_item"​ />
​ </LinearLayout>

With those values being sent in, our Pick Players screen now looks like this:

images/pd.fragments/pick-players-after-binding.png

The Pick Players form is finally taking shape! The screen is looking more and more like it will in the end, and with data binding enabled, it’ll be smoother to add additional logic here. The next step for this screen will be keeping track of what’s been entered for each user and having that available for setting up a game. The best way to handle this will be introducing a new concept called a ViewModel, which is a class that stores data for a given view and alerts the view if that data changes. But before we worry about ViewModel classes, we’re going to build out the other main fragment of the app, GameFragment.

👈 Build a Fragment (Pick Players) | TOC | Build Another Fragment (G ame) 👉

Kotlin and Android Development featuring Jetpack by Michael Fazio can be purchased in other book formats directly from the Pragmatic Programmers. If you notice a code error or formatting mistake, please let us know here so that we can fix it.

--

--

The Pragmatic Programmers
The Pragmatic Programmers

We create timely, practical books and learning resources on classic and cutting-edge topics to help you practice your craft and accelerate your career.