Home Android & Kotlin Tutorials

Advanced Data Binding in Android: Layout Expressions

Learn how to use layout expressions for data binding in Android and make your code more concise and less error-prone.

Version

  • Kotlin 1.5, Android 10.0, Android Studio 2020.3.1

In this tutorial, you’ll learn to use layout expressions for data binding in Android. It enables you to do some powerful stuff when binding layouts and makes your code more concise and less prone to error.

You’ll work on the Go-Buy application from Data Binding Getting started tutorial. It’s a simple app for making a shopping list. You’ll refactor it slightly and learn:

  • Which mathematical expressions you can use with data binding.
  • How to move simple logic from code to layout expressions.
  • An easy way to reference strings and pass them arguments in data binding.
  • How to handle view events like clicks with data binding.
Note: This tutorial assumes you know the basics of Android development with Kotlin. If you’re new to Kotlin, check our Kotlin introduction tutorial. If you’re new to Android development, read through our Beginning Android Development tutorials to familiarize yourself with the basics.

Getting Started

Download the starter project by clicking the Download Materials button at the top or bottom of the tutorial. Unzip it and import the starter project into Android Studio.

Wait for the Gradle sync to finish.

The project structure follows Model-View-Viewmodel (MVVM) architecture. There is one model GroceryItem that represents an item in the shopping list. GroceryListActivity shows the shopping list using GroceryAdapter. NewItemDialogFragment shows a dialog for adding an item to the shopping list.

Project structure

You continue where Getting Started tutorial left. Before you get your hands dirty, what was it that data binding does?

Recapping Data Binding

Android Data Binding Library is part of Android Jetpack and it simplifies binding your data classes to layout. You bind your classes in the layout XML file and set view properties there. That approach isolates the logic for showing data on views in one place. Using the traditional “findViewById” approach of connecting data and view, you would call methods on views from code to set the data shown to user. This can be error-prone. Using data binding reduces the amount of code and the risk of bugs.

To use data binding in your layout, the root tag must be layout. It must contain two elements:

  1. data element: used to describe data that will be shown in layout.
  2. view root element: this would be the root tag in a “regular” layout.

Data defined in the data element are used in expressions when setting the view properties using @{} syntax.

Seeing The Difference to View Binding

The Android documentation for Data Binding points to the main difference: “If you are using data binding primarily to replace findViewById() calls, consider using view binding instead.”

View Binding Library simplifies access to views from your Activities or Fragments.
Data Binding Library gives access to data classes in your layout files. Instead of setting data on each view from code, you pass the data class and set view properties in XML.

Diagram illustrating View binding and Data binding

Understanding Layout Expressions

Data Binding Library provides language for writing expressions in your layouts. This includes mathematical operations, concatenating, formatting and using strings from resources. This should be enough to implement a simple logic for showing and formatting your data within layouts. Be careful. This can be double-edged – adding too complex logic using layout expressions can make them unreadable. With great power comes great responsibility!

Using Mathematical and String Operators

The simplest and most used are mathematical expressions. Supported mathematical operators are + - / * %.

To test this you’ll move logic for calculating total price of items from GroceryAdapter to R.layout.grocery_list_item.

Open R.layout.grocery_list_item and replace data element them with:

<data>
  <variable
    name="item"
    type="com.raywenderlich.android.gobuy.model.GroceryItem" />
</data>

This enables you to bind GroceryItem objects that hold data for adapter items to layout and calculate the amount for an item (row in the Go-Buy item list).

Find TextView with id tv_grocery_item_name and replace android:text property with:

android:text='@{item.itemName + ": " + item.amount + "x"}'

This sets the text to item name followed by the amount, for example: Bread: 2x.

Note two critical items. First, the strings in layout expressions can be concatenated in a way similar to concatenating strings in code. Second, if you are using strings in layout expressions, open them with single quotes (‘) in order to use double quotes (“) inside the expression.

Next, add layout expression for showing single-item price and total price (item price multiplied by number of items). This time you’ll use String.format() to format prices.

Find TextView with id tv_grocery_item_price and again replace android:text property with:

android:text='@{"$" + String.format("%.2f", item.price) + "/$" + String.format("%.2f", item.price * item.amount)}'

Let’s go over this expression.

You are concatenating four parts: dollar sign, single-item price, dollar sign and total price.
Each price is formatted using String.format().

You can use most expressions that you can use in code. For a detailed list check the official documentation.

Note: This is as far as we’ll go with complicated expressions. For more complicated cases, consider doing them in code.

In order for the new layout expression to work, you need to pass GroceryItem instance to GroceryListItemBinding. Open GroceryAdapter and change bind function from ViewHolder to:

fun bind(item: GroceryItem) {
  binding.item = item
}

Build and run the app and tap “Add item” to add a new item to the shopping list:

Shows Add new item dialog in app

After adding, you can see the prices formatted using layout expressions.

App main screen with item added

Using String Resources With Parameters

You can also use strings from resources along with parameters in layout expression.

Open activity_grocery_list.xml and replace the definitions in data tags with:

<data>
    <variable
        name="total"
        type="Double" />
</data>

Find TextView with id total_text_view and replace android:text property with:

android:text="@{@string/money_amount(total)}"

This expression is equal to using getString(R.string.money_amount, total) in code.

Open GroceryListActivity and replace assignments of binding.totalAmount with:

binding.total = viewModel.getTotal()

deleteGroceryItem will look like:

private fun deleteGroceryItem(position: Int) {
  Log.d("GoBuy", "delete")
  viewModel.removeItem(position)
  binding.total = viewModel.getTotal()
  binding.rvGroceryList.adapter?.notifyDataSetChanged()
}

and onDialogPositiveClick:

override fun onDialogPositiveClick(dialog: DialogFragment, item: GroceryItem, isEdit: Boolean, position: Int?) {
  if (!isEdit) {
    viewModel.groceryListItems.add(item)
  } else {
    viewModel.updateItem(position!!, item)
    binding.rvGroceryList.adapter?.notifyDataSetChanged()
  }
  binding.total = viewModel.getTotal()
   Snackbar.make(binding.addItemButton, "Item Added Successfully", Snackbar.LENGTH_LONG).setAction("Action", null).show()
}

Build and run the app and add some items.

Invalid value for total on app main screen

Whoa! What’s that weird value displayed as total? That’s because total variable isn’t set initially.
To fix it, add the following at the end of onCreate:

binding.total = viewModel.getTotal()

Build and run again – the total value is now displayed properly.

Main screen with total set to zero

Handling Events from Views

Data binding allows you to handle view events too. You can link an event from view — like click to functions — using layout expressions.

There are two approaches:

  1. Method references: binding methods with matching signatures to view events.
  2. Listener bindings: lambda expressions that are evaluated when event is triggered.

Understanding Method References vs. Listener Bindings

The major difference between the two is method references create bindings at compile time, while listener bindings are evaluated at run time when the event is triggered. That means errors will be easier to detect when using method references because they are validated at compile time.

On the other hand, listener bindings provide increased flexibility because you can call any bound method from the lambda expression.

Handling Click Event Using Method Reference

You’ll test method references by creating and adding a method for handling click event on button for adding new items.

First, add a class that will hold the function that will be called on click.

Open GroceryListActivity and add the following class to it:

class Listeners(private val supportFragmentManager: FragmentManager) {

  fun onAddGroceryItemClick(view: View) {
    val newFragment = NewItemDialogFragment.newInstance(R.string.add_new_item_dialog_title, null)
    newFragment.show(supportFragmentManager, "newItem")
  }
}

Add imports for FragmentManager and View:

import android.view.View
import androidx.fragment.app.FragmentManager

Note that onAddGroceryItemClick needs to have one argument of type View. It needs to match onClick method signature from View.OnClickListener.

Open activity_grocery_list.xml and add the following variable in data tags:

<variable
    name="listeners"
    type="com.raywenderlich.android.gobuy.view.GroceryListActivity.Listeners" />

This binds the new Listeners class. Now you can bind the onAddGroceryItemClick from Listeners. Find Button with id add_item_button and add the following property to it:

android:onClick="@{listeners::onAddGroceryItemClick}"

This will wrap onAddGroceryItemClick in View.OnClickListener and set it on add_item_button.

Finally, bind the Listeners class in GroceryListActivity by adding the following to the end of onCreate:

binding.listeners = Listeners(supportFragmentManager)

Remove the code that sets the listener on addItemButton:

binding.addItemButton.setOnClickListener {
  addGroceryItem()
}

Your onCreate now looks like this:

override fun onCreate(savedInstanceState: Bundle?) {
  setTheme(R.style.AppTheme)

  super.onCreate(savedInstanceState)

  viewModel = ViewModelProviders.of(this).get(GroceryListViewModel::class.java)
  binding = DataBindingUtil.setContentView(this, R.layout.activity_grocery_list)

  binding.rvGroceryList.layoutManager = LinearLayoutManager(this)
  binding.rvGroceryList.adapter = GroceryAdapter(viewModel.groceryListItems, this, ::editGroceryItem, ::deleteGroceryItem)

  binding.total = viewModel.getTotal()
  binding.listeners = Listeners(supportFragmentManager)
}

Build and run the app and tap add button.

Shows Add new item dialog in app

You’ll see the dialog for adding an item.

Handling Events Via Listener Bindings

You’ll test listener bindings by adding listeners for item edit and delete button clicks. Open grocery_list_item.xml.

Add the following bindings in the data tags:

<variable
  name="position"
  type="Integer" />

<variable
    name="adapter"
    type="com.raywenderlich.android.gobuy.view.GroceryAdapter" />

This binds GroceryAdapter with listeners for edit and delete actions: itemEditListener and itemDeleteListener. Also, this binds the position that represents the position of the item being shown in the list. It will be passed to listeners from GroceryAdapter.

Find Button with id button_edit and add the listener binding for onClick event:

android:onClick="@{() -> adapter.itemEditListener.invoke(position)}"

The lambda expression invokes itemEditListener and passes the position of the item.

Find Button with id button_delete and add the listener binding for onClick event:

android:onClick="@{() -> adapter.itemDeleteListener.invoke(position)}"

Finally, open GroceryAdapter and add the following line to onCreateViewHolder:

binding.adapter = this

This assigns the adapter instance so its listeners can be called in onClick methods.

Build and run the app and add a few items. Try to delete one. What’s going on?

The app crashes after you tap edit or delete buttons for items. That’s because you didn’t bind the position of the item. This is one of the drawbacks because listener bindings are validated at runtime.

Add the following line in bind function of ViewHolder:

binding.position = adapterPosition

This assigns position of the item that was bound to the view.

Build and run the app.

Main screen with total set to zero

You can again edit and remove items from the list.

That’s it! You’ve mastered layout expressions. Use them wisely!

Where to Go From Here?

You can download the completed project files by clicking on the Download Materials button at the top or bottom of the tutorial.

Check out Data Binding tutorial if you haven’t. If you want to know more about Layout expression take look at the official documentation.

We hope you enjoyed this tutorial. If you have any questions or comments, please join the forum discussion below!

Reviews

More like this

Contributors

Comments