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. By Husayn Hakeem.

5 (4) · 1 Review

Download materials
Save for later
Share
You are currently viewing page 3 of 4 of this article. Click here to view the first page.

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!