Our Biggest Black Friday Sale — Ever!

Introducing unlimited access to all courses, all books, and our new monthly live professional development series! Just $899 $399 per year during our Black Friday event

Ends in... ::
Home Android & Kotlin Tutorials

Advanced Data Binding in Android: Observables

Learn how to use the Data Binding Library to bind UI elements in your XML layouts to data sources in your app using LiveData and StateFlow.

5/5 3 Ratings

Version

  • Kotlin 1.5, Android 4.4, Android Studio 2020.3.1

The Data Binding Library was built with observability in mind, a pattern that’s become quite popular in mobile app development.

Observability comes as a complement to data binding, whose basic concept just considers your view and data objects. However, it’s through this pattern that your data can automatically propagate its changes to the view. This removes the need to manually update your views whenever new data is available, thus simplifying your codebase and reducing boilerplate code.

In this tutorial, you’ll learn how to bind observable data sources in your layout. You’ll build the registration screen for SweatBar, a gym app. During the process, you’ll learn how to:

  • Set up observable data sources with both LiveData and StateFlow.
  • Make different types of data observable, including simple types, collections and objects.
  • Transform data from other sources and expose them to your layouts.
Note: This tutorial assumes you know the basics of Android development. If you’re new to Android development, check out the following tutorials: Beginning Android Development and Kotlin for Android: An Introduction.

Getting Started

Download the materials using the Download Materials button at the top or bottom of this tutorial. Open Android Studio and import the starter project.

Take a moment to familiarize yourself with the code. The main files are the following:

  • MainActivity.kt: An Activity where the user inputs their information to register. You’ll find this class in the livedata and stateflow packages. Both files are mostly the same, and you’ll only run one of them at any given time during this tutorial.
  • MainViewModel.kt: A ViewModel that contains the UI’s data. Similar to MainActivity, you’ll find this class in the livedata and stateflow packages. Both files are mostly the same, and you’ll only use one of them at any given time during this tutorial.
  • Session.kt: An Enum class with the different types of sessions a user can register for.
  • PhoneNumber.kt: A model that represents a phone number.
  • activity_main_*.xml: A layout file with the registration fields.

Build and run. You’ll see a screen where the user can input information to register. The UI doesn’t do much at the moment. You’ll work on improving that in the next sections!

Registration screen for SweatBar app

Observing Data Sources

When the Data Binding Library was shipped, it included observable classes for simple types, like ObservableBoolean, ObservableInt and ObservableDouble, as well as the generic ObservableField. These observable fields came with lifecycle-aware observability baked into them, as the Data Binding Library only updated Views when they were active.

Years later, the Jetpack Architecture components introduced another observable class, LiveData. In addition to being lifecycle-aware, it also supports transformations and other Architecture components, like Room and WorkManager, which is why using LiveData instead of the observable fields is now recommended.

Enabling Data Binding

You’ll now enable data binding in the project. Open the app’s build.gradle file. Inside the buildFeatures block, replace // TODO: enable data binding with the following:

buildFeatures {
  ...
  dataBinding true
}

Click Sync Now to sync the project with the Gradle file. You’re all set up!

Observing With LiveData

Note: Throughout this section, you’ll work with MainActivity and MainViewModel from the livedata package.

Start by setting up data binding in the Activity and XML layout.

Open activity_main_livedata.xml. Remove the TODO at the top of the file, then wrap the root ScrollView in a layout tag and import MainViewModel, which you’ll use for data binding:

<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">

  <data>
    <variable
      name="viewmodel"
      type="com.raywenderlich.android.databindingobservables.livedata.MainViewModel" />
  </data>

  <ScrollView...>
</layout>

Also, remove the following lines from the ScrollView as they are provided by the layout tag:

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"

Open MainActivity.kt, and replace // TODO: Set up data binding with the following:

val binding = DataBindingUtil.setContentView<ActivityMainLivedataBinding>(
    this,
    R.layout.activity_main_livedata
) // 1
binding.lifecycleOwner = this // 2
binding.viewmodel = viewModel // 3

Finally, make sure the following imports are at the top of the Activity:

import androidx.databinding.DataBindingUtil
import com.raywenderlich.android.databindingobservables.databinding.ActivityMainLivedataBinding

The code above sets:

  1. The content view of MainActivity.
  2. MainActivity as the LifecycleOwner to use for observing data in the binding. This controls when observing data starts and stops.
  3. The variable viewmodel of type MainViewModel you defined above in the layout file.

With this set up, you’re now ready to connect the app’s data sources to its UI!

Observing Simple Types

Simple types include primitive types such as Boolean, Int and Float as well as String. They aren’t observable by default, but an XML layout can observe them by simply wrapping them in LiveData. Emitting a new value through LiveData propagates it to the UI layout.

The registration form includes fields for the user’s first name, last name and email, which are all Strings. You’ll add corresponding data sources for them in MainViewModel.kt. Open MainViewModel.kt and replace // TODO: Add first name, last name and email with:


val firstName = MutableLiveData(DEFAULT_FIRST_NAME)
val lastName = MutableLiveData(DEFAULT_LAST_NAME)
val email = MutableLiveData(DEFAULT_EMAIL)

Now, open activity_main_livedata.xml and use these newly created observable fields in the EditTexts with the matching ids below:

<EditText
  android:id="@+id/firstNameEditText"
  ...
  android:text="@={viewmodel.firstName}" />

<EditText
  android:id="@+id/lastNameEditText"
  ...
  android:text="@={viewmodel.lastName}" />

<EditText
  android:id="@+id/emailEditText"
  ...
  android:text="@={viewmodel.email}" />

In the code above, you’re binding the value of the user’s first name, last name and email, respectively, to MainViewModel‘s firstName, lastName and email fields. This means that, for example, as the user updates their first name on the registration form, the value of firstName updates at the same time. The opposite is also valid: If firstName‘s value changes, it’s reflected in the UI. This is called two-way binding.

Omitting the = sign and only writing @{viewmodel.firstName} makes it a one-way binding instead, going from the firstName field to the UI. This means that as the user updates the first name on the registration form, firstName‘s value remains the same.

Observing Collections

Usually, collections hold data in an app. Think of lists of users or products and dictionaries of key/value preferences. Making a collection of data observable is similar to simple data — you just wrap it in LiveData.

The registration form you’re building should let the user pick the sessions they plan to enroll in. The Session enum class defines the different possible sessions users can pick from. If the user is an early bird and selects the MORNING session, you’ll store this information in a map: {MORNING: true}. When the user finishes the registration, this map will contain the sessions the user opted into and out of.

Open MainViewModel.kt, and replace // TODO: Add sessions with the following:

val sessions = MutableLiveData<EnumMap<Session, Boolean>>(
    EnumMap(Session::class.java)
).apply { // 1
  Session.values().forEach { value?.put(it, false) } // 2
}

If the IDE hasn’t done already, import these classes:

import java.util.EnumMap
import com.raywenderlich.android.databindingobservables.model.Session

Here’s what is happening doing in the code above:

  1. A map with keys of type Session and values of type Boolean is created. EnumMap is a map optimized for enum keys.
  2. The map is populated with all the possible sessions and setting their value to false, since by default the user isn’t enrolled in any sessions.

Next, bind sessions to the UI. Open activity_main_livedata.xml, locate the session Chips at the bottom of the file and update them as follows:

<com.google.android.material.chip.Chip
  android:id="@+id/morningSessionChip"
  ...
  android:checked="@={viewmodel.sessions[Session.MORNING]}" />

<com.google.android.material.chip.Chip
  android:id="@+id/afternoonSessionChip"
  ...
  android:checked="@={viewmodel.sessions[Session.NOON]}" />

<com.google.android.material.chip.Chip
  android:id="@+id/eveningSessionChip"
  ...
  android:checked="@={viewmodel.sessions[Session.EVENING]}" />

<com.google.android.material.chip.Chip
  android:id="@+id/nightSessionChip"
  ...
  android:checked="@={viewmodel.sessions[Session.NIGHT]}" />

In the code above, you’re binding the state of Chip, whether it’s checked or not, with the sessions field. When the user selects MORNING, the value of the key MORNING in the sessions map is set to true. Its value becomes false when the user deselects MORNING.

Before moving on, you may have noticed a compile error. This is because the XML layout doesn’t recognize the Session enum. To fix this, import the enum into your XML layout file as follows:

<data>
  <import type="com.raywenderlich.android.databindingobservables.model.Session" />
  ...
</data>

Once the enum is imported into the layout, the errors will disappear since it now knows what Session is referring to.

Observing Objects

An object isn’t observable by default. Even if you wrap it in LiveData, it still isn’t observable, meaning that if any attribute in that object changes, it doesn’t trigger the LiveData to emit the change. For an object to notify its observers when its attributes change, it must implement the Observable interface.

The Data Binding Library provides BaseObservable, a convenience class that implements the Observable interface and makes it easier to propagate changes to the class’s properties. This makes them usable directly from a layout file.

As part of the registration, the user must provide their phone number, which contains two parts: the area code and the rest of the number. This data structure is represented by PhoneNumber.

The first step to make PhoneNumber observable is to have it extend the BaseObservable class. Open PhoneNumber.kt, and update it as follows:

class PhoneNumber: BaseObservable() {
  ...
}

You’ll also need to import the following if the IDE hasn’t informed you:

import androidx.databinding.BaseObservable

Whenever any of the class’s properties change, it must notify its observers. Replace the TODO in this file with the following:

class PhoneNumber : BaseObservable() {

  @get:Bindable // 1
  var areaCode: String = ""
    set(value) {
      field = value
      notifyPropertyChanged(BR.areaCode) // 2
    }

  @get:Bindable
  var number: String = ""
    set(value) {
      field = value
      notifyPropertyChanged(BR.number)
    }
}

Also import the following if the IDE doesn’t inform you:

import androidx.databinding.Bindable
import com.raywenderlich.android.databindingobservables.BR

Here’s what’s happening in the code above:

  1. You annotate areaCode with the Bindable annotation. This lets the Data Binding Library generate an entry for it in a class, BR.java. This entry is a static immutable integer field of the same name, areaCode, and it identifies when PhoneNumber‘s areaCode field changes.
  2. When areaCode‘s value changes, you propagate the change to notify any observers. You do this by using areaCode‘s generated field in BR.java.

You’ll notice compile errors in PhoneNumber.kt, since the compiler can’t find BR‘s fields yet. Rebuild the project, and the Data Binding Library will generate the BR class with the appropriate fields.

It’s time to use this class. Open MainViewModel.kt, and replace // TODO: Add phone number with the following:

val phoneNumber = PhoneNumber()

Make sure to import the following if the IDE hasn’t done so:

import com.raywenderlich.android.databindingobservables.model.PhoneNumber

Now, bind this new instance to the UI. Open activity_main_livedata.xml, locate the phone number’s EditText fields, and update them as follows:

<EditText
  android:id="@+id/phoneNumberAreaCodeEditText"
  ...
  android:text="@={viewmodel.phoneNumber.areaCode}" />

<EditText
  android:id="@+id/phoneNumberEditText"
  ...
  android:text="@={viewmodel.phoneNumber.number}" />

Now, as you edit the phone number in the registration form, it also updates the corresponding instance in MainViewModel.

Transforming a Single Data Source

As previously mentioned, using LiveData over the old observable fields offers the option of using transformations. The data source your UI observes can itself observe another data source. That could be another data source the UI is observing or even data streams from other components, like a database. Your data source can then transform data it receives before preparing data for the UI and emitting it.

In the registration form, once the user inputs their email address, you’ll use it to both generate and display their username. You’ll use LiveData‘s Transformations API to accomplish this.

Open MainViewModel.kt, and replace // TODO: Add username with the following:

val showUsername: LiveData<Boolean> = Transformations.map(email, ::isValidEmail)
val username: LiveData<String> = Transformations.map(email, ::generateUsername)

Make sure to import the following:

import androidx.lifecycle.Transformations
import com.raywenderlich.android.databindingobservables.utils.isValidEmail

In the code above:

  1. You use the email property, which is a LiveData instance, to control whether to display or hide the username on the UI. When the user’s email is valid, showUsername emits true to show the username. But when the email is invalid, showUsername emits false to hide it.
  2. Whenever email‘s value changes, generateUsername uses this latest value to generate a new username, which username then emits.

Now, you’ll use these two fields in your layout. Open activity_main_livedata.xml, locate the username’s TextView, and update it as follows:

<TextView
  android:id="@+id/usernameTextView"
  ...
  android:text="@{@string/username_format(viewmodel.username)}" // 1
  android:visibility="@{viewmodel.showUsername ? View.VISIBLE : View.GONE}" /> // 2

Adding the two lines above causes a compile error in your layout file, as the compiler doesn’t know what View is. Import it at the top of the file:

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

In the code above, you:

  1. Set up a one-way binding between username and TextView‘s text. Whenever a new username is emitted, TextView‘s text is recomputed. username_format is just a string resource that formats the username, which it takes as an argument.
  2. Use a ternary operator to show or hide TextView depending on the value of showUsername.

Build and run the app, and enter an invalid email address — you’ll see the username doesn’t show up. Now, enter a valid email address — the username is there. Magic!

SweatBar app with valid email entered and username displayed

Transforming Multiple Data Sources

The beauty of LiveData transformations is that you don’t have to limit yourself to a single source — you can transform multiple sources. AndroidX provides a convenience LiveData subclass to achieve this, MediatorLiveData. It can observe multiple sources and perform an operation when any of them emits a new value.

After providing all required information, the user should be able to click REGISTER. Currently, the button is always enabled. Instead, it should be disabled until the user has input all necessary information. This includes the user’s first name, last name and email.

Open MainViewModel.kt, and replace // TODO: Add a way to enable the registration button with the following:

val enableRegistration: LiveData<Boolean> = MediatorLiveData<Boolean>().apply { // 1
  addSources(firstName, lastName, email) { // 2
    value = isUserInformationValid() // 3
  }
}

Here’s what’s happening in the code above:

  1. You create a new MediatorLiveData instance that emits Booleans: true to enable the registration button and false to disable it.
  2. You observe the required user information fields: firstName, lastName and email.
  3. Whenever the value of any of the fields changes, you emit a new Boolean, indicating whether or not to enable the registration button.

As you may have noticed, isUserInformationValid always returns false. Now, update it as follows:

private fun isUserInformationValid(): Boolean {
  return !firstName.value.isNullOrBlank()
      && !lastName.value.isNullOrBlank()
      && isValidEmail(email.value)
}

Finally, bind the button’s state to this new field. Open activity_main_livedata.xml, locate the registration button and update it as follows:

<Button
  android:id="@+id/registerButton"
  ...
  android:enabled="@{viewmodel.enableRegistration}" // 1
  android:onClick="@{(view) -> viewmodel.onRegisterClicked()}" /> // 2

With the code above, you set up:

  1. A one-way binding between enableRegistration and the button’s state.
  2. A click listener on the button that calls onRegisterClicked when the button is enabled and the user clicks it.

Check out onRegisterClicked. It lets MainActivity display a success dialog and should log the user’s information. getUserInformation doesn’t do much at the moment, so update its body as follows:

private fun getUserInformation(): String {
  return "User information:\n" +
      "First name: ${firstName.value}\n" +
      "Last name: ${lastName.value}\n" +
      "Email: ${email.value}\n" +
      "Username: ${username.value}\n" +
      "Phone number: ${phoneNumber.areaCode}-${phoneNumber.number}\n" +
      "Sessions: ${sessions.value}\n"
}

The code above lets you review the information the user entered. In a real app, you’d probably send this information to the app’s server.

Build and run. Notice the registration button remains disabled until you enter all the required data. When it’s enabled, click it — the registration flow is now complete!

SweatBar app with registration success dialog

Observing With StateFlow

Instead of using LiveData, it may make more sense for your app to use data binding with StateFlow if you’re already using Kotlin and coroutines. This helps keep your codebase more consistent and may provide additional benefits compared to using LiveData, such as performing asynchronous logic in your data sources with the help of coroutines.

Note: Throughout this section, you’ll work with MainActivity and MainViewModel from the stateflow package.

Using StateFlow as the data binding source looks similar to using LiveData.

Open AndroidManifest.xml. Comment out intent-filter from .livedata.MainActivity and uncomment intent-filter from .stateflow.MainActivity as follows:

<!-- LiveData Activity -->
<activity
  android:name=".livedata.MainActivity"
  ...
<!--  <intent-filter>-->
<!--    <action android:name="android.intent.action.MAIN" />-->
<!--    <category android:name="android.intent.category.LAUNCHER" />-->
<!--  </intent-filter>-->
</activity>

<!-- StateFlow Activity -->
<activity
  android:name=".stateflow.MainActivity"
  ...
  <intent-filter>
    <action android:name="android.intent.action.MAIN" />
    <category android:name="android.intent.category.LAUNCHER" />
  </intent-filter>
</activity>

.stateflow.MainActivity is now the default launcher activity.

Set up data binding in MainActivity.kt and replace // TODO: Set up data binding with the following:

val binding = DataBindingUtil.setContentView<ActivityMainStateflowBinding>(
    this, 
    R.layout.activity_main_stateflow
)
binding.lifecycleOwner = this
binding.viewmodel = viewModel

Add the following imports:

import androidx.databinding.DataBindingUtil
import com.raywenderlich.android.databindingobservables.databinding.ActivityMainStateflowBinding

Lastly, open activity_main_stateflow.xml, and remove the TODO at the top of the file. Then, wrap the root ScrollView in a layout tag and import MainViewModel, which you’ll use for data binding.

<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">

  <data>
    <variable
      name="viewmodel"
      type="com.raywenderlich.android.databindingobservables.stateflow.MainViewModel" />
  </data>

  <ScrollView...>
</layout>

Also, remove the following lines from ScrollView:

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"

Observing Simple Types

Like wrapping simple types with LiveData, you can make your simple typed data observable by wrapping it in StateFlow. Implement the data sources for the user’s first name, last name and email in MainViewModel.kt. It’ll look like this:

val firstName = MutableStateFlow(DEFAULT_FIRST_NAME)
val lastName = MutableStateFlow(DEFAULT_LAST_NAME)
val email = MutableStateFlow(DEFAULT_EMAIL)

Similar to how you bound these fields to the UI in the LiveData section, use these fields in activity_main_stateflow.xml as you did in activity_main_livedata.xml:

<EditText
  android:id="@+id/firstNameEditText"
  ...
  android:text="@={viewmodel.firstName}" />

<EditText
  android:id="@+id/lastNameEditText"
  ...
  android:text="@={viewmodel.lastName}" />

<EditText
  android:id="@+id/emailEditText"
  ...
  android:text="@={viewmodel.email}" />

Observing Collections

Similar to wrapping collections with LiveData, you can make a collection observable by wrapping it in StateFlow. Like above, implement the sessions data source in MainViewModel.kt.

val sessions = MutableStateFlow<EnumMap<Session, Boolean>>(EnumMap(Session::class.java)).apply {
  Session.values().forEach { value[it] = false }
}

Import the following:

import java.util.EnumMap
import com.raywenderlich.android.databindingobservables.model.Session

The code above should look familiar to you: It’s almost the exact code you used to set up sessions in the LiveData section. You’ll bind it to the layout the same way you did before. Open activity_main_stateflow.xml and update the chips to use the sessions StateFlow:

<com.google.android.material.chip.Chip
  android:id="@+id/morningSessionChip"
  ...
  android:checked="@={viewmodel.sessions[Session.MORNING]}" />

<com.google.android.material.chip.Chip
  android:id="@+id/afternoonSessionChip"
  ...
  android:checked="@={viewmodel.sessions[Session.NOON]}" />

<com.google.android.material.chip.Chip
  android:id="@+id/eveningSessionChip"
  ...
  android:checked="@={viewmodel.sessions[Session.EVENING]}" />

<com.google.android.material.chip.Chip
  android:id="@+id/nightSessionChip"
  ...
  android:checked="@={viewmodel.sessions[Session.NIGHT]}" />

You’ll also need to import the enum at the top of the layout:

<data>
  <import type="com.raywenderlich.android.databindingobservables.model.Session" />
  ...
</data>

Observing Objects

Whether you’re using LiveData or StateFlow, the approach to making an object observable remains the same. You’ve already made PhoneNumber extend BaseObservable in the previous section. All that’s left is to add a phoneNumber field in MainViewModel.kt and bind it to the phone number EditTexts in the layout file.

Just like before, open MainViewModel.kt, and replace // TODO: Add phone number with the following:

val phoneNumber = PhoneNumber()

Open activity_main_stateflow.xml, locate the phone number’s EditText fields, and update them as follows:

<EditText
  android:id="@+id/phoneNumberAreaCodeEditText"
  ...
  android:text="@={viewmodel.phoneNumber.areaCode}" />

<EditText
  android:id="@+id/phoneNumberEditText"
  ...
  android:text="@={viewmodel.phoneNumber.number}" />

Transforming a Single Data Source

StateFlow provides many operators to transform a data source. They let you do much more than just map data — you can also filter, debounce and collect data, to name a few. Compared to LiveData, you have more control over how you transform your data sources.

For your use case, you’ll only need the mapping operator. You’ll use a convenience method mapToStateFlow in MainViewModel.kt to generate a username and decide when to show it. Replace // TODO: Add username with the following:

val showUsername: StateFlow<Boolean> = email.mapToStateFlow(::isValidEmail, DEFAULT_SHOW_USERNAME)
val username: StateFlow<String> = email.mapToStateFlow(::generateUsername, DEFAULT_USERNAME)

You may also need to import the following:

import com.raywenderlich.android.databindingobservables.utils.isValidEmail

As you may have noticed, unlike LiveData, StateFlow requires an initial value.

Next, bind these fields in activity_main_stateflow.xml:

<TextView
  android:id="@+id/usernameTextView"
  ...
  android:text="@{@string/username_format(viewmodel.username)}" // 1
  android:visibility="@{viewmodel.showUsername ? View.VISIBLE : View.GONE}" /> // 2

Finally, add this import to the data tag at the top:

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

Build and run. Enter a valid email address, and you’ll see the generated username displays.

Transforming Multiple Data Sources

You can combine multiple data sources and transform their emitted values using — you guessed it — the combine method! It takes in multiple Flows and returns a Flow whose values are generated with a transform function that combines the most recently emitted values by each flow. Since data binding doesn’t recognize Flows, you’ll convert the returned Flow to a StateFlow.

In this last step, you’ll set the state of the registration button depending on the required fields: first name, last name and email.

val enableRegistration: StateFlow<Boolean> = combine(firstName, lastName, email) { _ ->
  isUserInformationValid()
}.toStateFlow(DEFAULT_ENABLE_REGISTRATION)

Then, import the following:

import kotlinx.coroutines.flow.combine

This should seem similar to what you implemented with LiveData. You’re observing the first name, last name and email flows, and whenever any of them emit a value, you call isUserInformationValid() to enable or disable the registration button.

Just like before, update isUserInformationValid() with the following code:

private fun isUserInformationValid(): Boolean {
  return !firstName.value.isNullOrBlank()
      && !lastName.value.isNullOrBlank()
      && isValidEmail(email.value)
}

The last step is binding this field to the state of the registration button and setting its click listener.

Open activity_main_stateflow.xml, and update the register button to look as follows:

<Button
  android:id="@+id/registerButton"
  ...
  android:enabled="@{viewmodel.enableRegistration}"
  android:onClick="@{(view) -> viewmodel.onRegisterClicked()}" />

You also need to update getUserInformation:

private fun getUserInformation(): String {
  return "User information:\n" +
      "First name: ${firstName.value}\n" +
      "Last name: ${lastName.value}\n" +
      "Email: ${email.value}\n" +
      "Username: ${username.value}\n" +
      "Phone number: ${phoneNumber.areaCode}-${phoneNumber.number}\n" +
      "Sessions: ${sessions.value}\n"
}

Build and run, then play around with the registration form. Once you’ve entered the required data, click the registration button, and the success dialog appears. That’s it — you’ve done it again!

Where to Go From Here?

Download the final project using the Download Materials button at the top or bottom of the tutorial.

Congratulations! You learned how to use one of data binding’s most important features: observability. You saw how to set up your data sources to be observable using either LiveData or StateFlow, and you also learned how to make different types of data observable, from primitives to collections and complex objects. Finally, you saw how to set up observable data sources that expose transformed data from other sources.

If you need to brush up on data binding, check out Data Binding in Android: Getting Started. If, instead, you’d like to see how data binding fits in with other architectural patterns in Android, check out MVVM and DataBinding: Android Design Patterns.

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

More like this

Contributors

Comments