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 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:
- You annotate
areaCode
with theBindable
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 whenPhoneNumber
‘sareaCode
field changes. - When
areaCode
‘s value changes, you propagate the change to notify any observers. You do this by usingareaCode
‘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:
- You use the
email
property, which is aLiveData
instance, to control whether to display or hide the username on the UI. When the user’s email is valid,showUsername
emitstrue
to show the username. But when the email is invalid,showUsername
emitsfalse
to hide it. - Whenever
email
‘s value changes,generateUsername
uses this latest value to generate a new username, whichusername
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:
- Set up a one-way binding between
username
andTextView
‘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. - Use a ternary operator to show or hide
TextView
depending on the value ofshowUsername
.
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!
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:
- You create a new
MediatorLiveData
instance that emits Booleans:true
to enable the registration button andfalse
to disable it. - You observe the required user information fields:
firstName
,lastName
andemail
. - 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:
- A one-way binding between
enableRegistration
and the button’s state. - 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!