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.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
Advanced Data Binding in Android: Observables
25 mins
- Getting Started
- Observing Data Sources
- Enabling Data Binding
- Observing With LiveData
- Observing Simple Types
- Observing Collections
- Observing Objects
- Transforming a Single Data Source
- Transforming Multiple Data Sources
- Observing With StateFlow
- Observing Simple Types
- Observing Collections
- Observing Objects
- Transforming a Single Data Source
- Transforming Multiple Data Sources
- Where to Go From Here?
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.
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 EditText
s 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 Flow
s 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 Flow
s, 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!