RxJava 2.0 is a popular reactive programming library that’s helped countless Android developers create highly responsive apps, using less code and less complexity, especially when it comes to managing multiple threads.
If you’re one of the many developers who’s made the switch to Kotlin, then it doesn’t mean you need to give up on RxJava!
In the first part of this series, I showed you how to move from programming with RxJava 2.0 in Java, to programming with RxJava in Kotlin. We also looked at how to banish boilerplate from your projects, by taking advantage of RxKotlin’s extension functions, and the secret to dodging the SAM conversion problem that many developers encounter when they first start using RxJava 2.0 with Kotlin.
In this second instalment, we’ll be concentrating on how RxJava can help solve the issues you’ll encounter in real-life Android projects, by creating a reactive Android application using RxJava 2.0, RxAndroid and RxBinding.
How Can I Use RxJava in Real-World Projects?
In our Reactive Programming with RxJava and RxKotlin article, we created some simple Observables
and Observers
that print data to Android Studio’s Logcat – but this isn’t how you’ll use RxJava in the real world.
In this article I’m going to show you how to use RxJava to create a screen that’s used in countless Android applications: the classic Sign Up screen.
If your app has any kind of Sign Up experience, then it’ll typically have strict rules about the kind of information it accepts. For example, maybe the password needs to exceed a certain number of characters, or the email address must be in a valid email format.
While you could check the user’s input once they hit the Sign Up button, this isn’t the best user experience, as it leaves them open to submitting information that’s clearly never going to be accepted by your application.
It’s far better to monitor the user as they’re typing, and then give them a heads-up as soon as it becomes clear they’re entering information that doesn’t meet your app’s requirements. By providing this kind of live and ongoing feedback, you give the user the opportunity to correct their mistakes before hitting that Sign Up button.
While you could monitor user activity using vanilla Kotlin, we can deliver this functionality using much less code, by enlisting the help of RxJava, plus a few other related libraries.
Creating the User Interface
Let’s start by building our user interface. I’m going to add the following:
- Two
EditTexts
, where the user can enter their email address (enterEmail
) and password (enterPassword
). - Two
TextInputLayout
wrappers, which will surround ourenterEmail
andenterPassword
EditTexts
. These wrappers will display a warning whenever the user enters an email address or password that doesn’t meet our app’s requirements. - A password visibility button, which allows the user to toggle between masking the password and viewing it as plain text.
- A Sign up button. To help keep this example focused on RxJava, I won’t be implementing this part of the sign up experience, so I’ll be marking this button as disabled.
Here’s my finished layout:
You can copy/paste this into your app if you want, or you can just download the project source code from our GitHub repo.
Creating a Reactive Sign-in Experience With Kotlin
Now let’s look at how we can use RxJava, plus a few related libraries, to monitor user input and provide feedback in real time.
I’ll be tackling the Sign Up screen in two parts. In the first section, I’ll show you how to use the RxBinding library to register and respond to text change events. In the second section, we’ll create some transformation functions that validate the user’s input, and then display an error message where appropriate.
Create a new project with the settings of your choice, but when prompted make sure you select the Include Kotlin Support checkbox.
Responding to Text Change Events
In this section, we’ll implement the following functionality:
- Detect when the user is typing in the
enterEmail
field. - Ignore all text change events that occur within a short space of time, as this indicates that the user is still typing.
- Perform an action when the user stops typing. In our finished app, this is where we’ll validate the user’s input, but in this section I’ll just be displaying a
Toast
.
1. RxBinding
RxBinding is a library that makes it easier to convert a wide range of UI events into Observables, at which point you can treat them just like any other RxJava data stream.
We’re going to monitor text change events, by combining RxBinding’s widget.RxTextView
with the afterTextChangeEvents
method, for example:
RxTextView.afterTextChangeEvents(enterEmail)
The problem with treating text change events as data streams, is that initially both the enterEmail
and enterPassword EditTexts
will be empty, and we don’t want our app to react to this empty state as though it’s the first data emission in the stream. RxBinding solves this problem by providing a skipInitialValue()
method, which we’ll use to instruct each Observer to ignore their stream’s initial value.
RxTextView.afterTextChangeEvents(enterEmail) .skipInitialValue()
I look at the RxBinding library in greater detail in my RxJava 2 for Android Apps article.
2. RxJava’s .debounce()
Operator
To deliver the best user experience, we need to display any relevant password or email warnings after the user has finished typing, but before they hit the Sign Up button.
Without RxJava, identifying this narrow window of time would typically require us to implement a Timer
, but in RxJava we just need to apply the debounce()
operator to our data stream.
I’m going to use the debounce()
operator to filter out all text change events that happen in quick succession, i.e. when the user is still typing. Here, we’re ignoring all text change events that happen within the same 400 milliseconds window:
RxTextView.afterTextChangeEvents(enterEmail) .skipInitialValue() .debounce(400, TimeUnit.MILLISECONDS)
3. RxAndroid’s AndroidSchedulers.mainThread()
The RxAndroid library’s AndroidSchedulers.mainThread
gives us an easy way to switch to Android’s all-important main UI thread.
Since it’s only possible to update Android’s UI from the main UI thread, we need to make sure we’re in this thread before we attempt to display any email or password warnings, and before we display our Toast
.
RxTextView.afterTextChangeEvents(enterEmail) .skipInitialValue() .debounce(400, TimeUnit.MILLISECONDS) .observeOn(AndroidSchedulers.mainThread())
4. Subscribe
To receive the data being emitted by enterEmail
, we need to subscribe to it:
RxTextView.afterTextChangeEvents(enterEmail) .skipInitialValue() .debounce(400, TimeUnit.MILLISECONDS) .observeOn(AndroidSchedulers.mainThread()) .subscribe {
5. Display the Toast
Eventually, we want our application to respond to text change events by validating the user’s input, but to help keep things straightforward, at this point I’m simply going to display a Toast
.
Your code should look something like this:
import android.support.v7.app.AppCompatActivity import android.os.Bundle import android.widget.Toast import com.jakewharton.rxbinding2.widget.RxTextView import kotlinx.android.synthetic.main.activity_main.* import io.reactivex.android.schedulers.AndroidSchedulers import java.util.concurrent.TimeUnit class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) RxTextView.afterTextChangeEvents(enterEmail) .skipInitialValue() .debounce(400, TimeUnit.MILLISECONDS) .observeOn(AndroidSchedulers.mainThread()) .subscribe { Toast.makeText(this, "400 milliseconds since last text change", Toast.LENGTH_SHORT).show() } } }
6. Update Your Dependencies
Since we’re using a few different libraries, we need to open our project’s build.gradle file and add RxJava, RxBinding and RxAndroid as project dependencies:
dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" implementation 'com.android.support:design:28.0.0-alpha1' implementation 'com.android.support:appcompat-v7:28.0.0-alpha1' implementation 'com.android.support.constraint:constraint-layout:1.1.0' //Add the RxJava dependency// implementation 'io.reactivex.rxjava2:rxjava:2.1.9' //Add the RxAndroid dependency// implementation 'io.reactivex.rxjava2:rxandroid:2.0.2' //Add the RxBinding dependency// implementation 'com.jakewharton.rxbinding2:rxbinding:2.1.1' }
You can test this part of your project, by installing it on your physical Android smartphone or tablet, or Android Virtual Device (AVD). Select the enterEmail
EditText
and start typing; a Toast
should appear when you stop typing.
Validating the User’s Input With Transformation Functions
Next, we need to lay down some ground rules about the kind of input our application will accept, then check the user’s input against this criteria and display an error message where appropriate.
Checking the user’s email or password is a multi-step process, so to make our code easier to read, I’m going to combine all of these steps into their own transformation function.
Here’s the start of the validateEmail
transformation function:
//Define an ObservableTransformer. Input and output must be a string// private val validateEmailAddress = ObservableTransformer{ observable -> //Use flatMap to apply a function to every item emitted by the Observable// observable.flatMap { //Trim any whitespace at the beginning and end of the user’s input// Observable.just(it).map { it.trim() } //Check whether the input matches Android’s email pattern// .filter { Patterns.EMAIL_ADDRESS.matcher(it).matches() }
In the above code, we’re using the filter()
operator to filter the Observable’s output based on whether it matches Android’s Patterns.EMAIL_ADDRESS
pattern.
In the next part of the transformation function, we need to specify what happens if the input doesn’t match the EMAIL_ADDRESS
pattern. By default, every unrecoverable error will trigger a call to onError()
, which terminates the data stream. Instead of ending the stream, we want our application to display an error message, so I’m going to use onErrorResumeNext
, which instructs the Observable to respond to an error by passing control to a new Observable, rather than invoking onError()
. This allows us to display our custom error message.
//If the user’s input doesn’t match the email pattern, then throw an error// .singleOrError() .onErrorResumeNext { if (it is NoSuchElementException) { Single.error(Exception("Please enter a valid email address")) } else { Single.error(it) } } .toObservable() } }
The final step, is to apply this transformation function to the email data stream, using the .compose()
operator. At this point, your MainActivity.kt should look something like this:
import android.support.v7.app.AppCompatActivity import android.os.Bundle import android.util.Patterns import io.reactivex.Observable import io.reactivex.ObservableTransformer import io.reactivex.Single import io.reactivex.android.schedulers.AndroidSchedulers import kotlinx.android.synthetic.main.activity_main.* import java.util.concurrent.TimeUnit import com.jakewharton.rxbinding2.widget.RxTextView class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) RxTextView.afterTextChangeEvents(enterEmail) .skipInitialValue() .map { emailError.error = null it.view().text.toString() } .debounce(400, //Make sure we’re in Android’s main UI thread// TimeUnit.MILLISECONDS).observeOn(AndroidSchedulers.mainThread()) .compose(validateEmailAddress) .compose(retryWhenError { passwordError.error = it.message }) .subscribe() } //If the app encounters an error, then try again// private inline fun retryWhenError(crossinline onError: (ex: Throwable) -> Unit): ObservableTransformer= ObservableTransformer { observable -> observable.retryWhen { errors -> //Use the flatmap() operator to flatten all emissions into a single Observable// errors.flatMap { onError(it) Observable.just("") } } } //Define an ObservableTransformer, where we’ll perform the email validation// private val validateEmailAddress = ObservableTransformer { observable -> observable.flatMap { Observable.just(it).map { it.trim() } //Check whether the user input matches Android’s email pattern// .filter { Patterns.EMAIL_ADDRESS.matcher(it).matches() } //If the user’s input doesn’t match the email pattern, then throw an error// .singleOrError() .onErrorResumeNext { if (it is NoSuchElementException) { Single.error(Exception("Please enter a valid email address")) } else { Single.error(it) } } .toObservable() } } }
Install this project on your Android device or AVD, and you’ll find that the email portion of the Sign Up screen is now checking your input successfully. Try entering anything other than an email address, and the app will warn you that this isn’t a valid input.
Rinse and Repeat: Checking the User’s Password
At this point, we have a fully-functioning enterEmail
field – and implementing enterPassword
is mostly just a case of repeating the same steps.
In fact, the only major difference is that our validatePassword
transformation function needs to check for different criteria. I’m going to specify that the user’s password input must be at least 7 characters long:
.filter { it.length > 7 }
After repeating all of the previous steps, the completed MainActivity.kt should look something like this:
import android.support.v7.app.AppCompatActivity import android.os.Bundle import android.util.Patterns import io.reactivex.Observable import io.reactivex.ObservableTransformer import io.reactivex.Single import io.reactivex.android.schedulers.AndroidSchedulers import kotlinx.android.synthetic.main.activity_main.* import java.util.concurrent.TimeUnit import com.jakewharton.rxbinding2.widget.RxTextView class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) //Respond to text change events in enterEmail// RxTextView.afterTextChangeEvents(enterEmail) //Skip enterEmail’s initial, empty state// .skipInitialValue() //Transform the data being emitted// .map { emailError.error = null //Convert the user input to a String// it.view().text.toString() } //Ignore all emissions that occur within a 400 milliseconds timespan// .debounce(400, //Make sure we’re in Android’s main UI thread// TimeUnit.MILLISECONDS).observeOn(AndroidSchedulers.mainThread()) //Apply the validateEmailAddress transformation function// .compose(validateEmailAddress) //Apply the retryWhenError transformation function// .compose(retryWhenError { emailError.error = it.message }) .subscribe() //Rinse and repeat for the enterPassword EditText// RxTextView.afterTextChangeEvents(enterPassword) .skipInitialValue() .map { passwordError.error = null it.view().text.toString() } .debounce(400, TimeUnit.MILLISECONDS).observeOn(AndroidSchedulers.mainThread()) .compose(validatePassword) .compose(retryWhenError { passwordError.error = it.message }) .subscribe() } //If the app encounters an error, then try again// private inline fun retryWhenError(crossinline onError: (ex: Throwable) -> Unit): ObservableTransformer= ObservableTransformer { observable -> observable.retryWhen { errors -> ///Use the flatmap() operator to flatten all emissions into a single Observable// errors.flatMap { onError(it) Observable.just("") } } } //Define our ObservableTransformer and specify that the input and output must be a string// private val validatePassword = ObservableTransformer { observable -> observable.flatMap { Observable.just(it).map { it.trim() } //Only allow passwords that are at least 7 characters long// .filter { it.length > 7 } //If the password is less than 7 characters, then throw an error// .singleOrError() //If an error occurs.....// .onErrorResumeNext { if (it is NoSuchElementException) { //Display the following message in the passwordError TextInputLayout// Single.error(Exception("Your password must be 7 characters or more")) } else { Single.error(it) } } .toObservable() } } //Define an ObservableTransformer, where we’ll perform the email validation// private val validateEmailAddress = ObservableTransformer { observable -> observable.flatMap { Observable.just(it).map { it.trim() } //Check whether the user input matches Android’s email pattern// .filter { Patterns.EMAIL_ADDRESS.matcher(it).matches() } //If the user’s input doesn’t match the email pattern...// .singleOrError() .onErrorResumeNext { if (it is NoSuchElementException) { ////Display the following message in the emailError TextInputLayout// Single.error(Exception("Please enter a valid email address")) } else { Single.error(it) } } .toObservable() } } }
Install this project on your Android device or AVD, and experiment with typing into the enterEmail
and enterPassword
fields. If you enter a value that doesn’t meet the app’s requirements, then it’ll display the corresponding warning message, without you having to tap the Sign Up button.
You can download this complete project from GitHub.
Conclusion
In this article, we looked at how RxJava can help solve the real-world problems you’ll encounter when developing your own Android applications, by using RxJava 2.0, RxBinding and RxAndroid to create a Sign Up screen.
For more background information about the RxJava library, be sure to check out our Get Started With RxJava 2.0 article.
Powered by WPeMatico