17.
RxBindings
Written by Alex Sullivan
In the last chapter, you learned all about wrapping existing APIs to make them into Observables. Hopefully, you’ve realized how powerful it is to express a lot of the framework APIs in reactive terms. Unfortunately, it’s a fair amount of repetitive work to wrap all of these frameworks.
It’s not too bad to make a reactive extension for, say, a Button. And it’s not too bad to make a reactive extension for an EditText. But, as you keep going, it starts to become a bit laborious to keep making these reactive wrappers.
There’s an extremely handy library called RxBindings, which takes care of making reactive bindings for all of the Android view classes. So good news! You get to be lazy and rely on a library to make those extensions for you. And as we all know, programming is 1% creativity and 99% laziness.
In this chapter, you’ll revisit the HexColor app and improve on it by using the RxBindings library.
Getting started
Open the starter project and run the app. You should see the HexColor app from Chapter 15, “Testing RxJava Code.”
Feel free to tap around. You can type in a hex code, and the background will change to that color. It will also show the RGB value and if you type in one of the colors in the ColorName enum, the name will show, too.
There’s a few limitations to the app, though. First off, most colors you enter don’t have an associated color name in the ColorName enum. You can see this list in X.kt.
That’s a hard nut to crack, since there’s a near infinite number of color combinations you can use in the app. Next up, manually tapping the digits can be a bit burdensome. It’d be nice if you could also use the keyboard to enter a new hex color.
In this chapter, you’ll work through solving both of these problems while also using the RxBinding library to make the Android view components a bit more reactive.
Extending ValueAnimator to be reactive
Speaking of making things more reactive, take a look at the animateColorChange method in ColorActivity. It’s the method that’s responsible for that fancy color changing animation. It’s a pretty great method, but it’s not very reactive. In the spirit of building on the work you did last chapter, you’re going to wrap that call in a reactive wrapper to make it fit better with the rest of the reactive app.
Open the AnimationUtils.kt file and look at the colorAnimator method:
fun colorAnimator(fromColor: Int, toColor: Int): Observable<Int> {
return Observable.empty()
}
colorAnimator takes two arguments: an integer named fromColor representing the starting color and another integer named toColor representing the ending color. The idea, here, is to convert the animateColorChange method to use this colorAnimator Observable instead of using a ValueAnimator the way it does now.
Replace the return Observable.empty() line with the following:
// 1
val valueAnimator =
ValueAnimator.ofObject(ArgbEvaluator(), fromColor, toColor)
valueAnimator.duration = 250 // milliseconds
// 2
val observable = Observable.create<Int> { emitter ->
// 3
valueAnimator.addUpdateListener {
emitter.onNext(it.animatedValue as Int)
}
}
Here’s a breakdown of the above code:
-
Create a new
ValueAnimatorusing theArgbEvaluatorto go from thefromColorto thetoColor. In case you’re not familiar withValueAnimator, it provides a handy way to get interpolated values between two values you provide, that you can later use to animate some view between them. TheArgbEvaluatorinterpolates two colorInts to make a smooth transition. -
Create a new Observable via the
createfunction. The Observable will emitInts since that’s what type theValueAnimatordeclared above will output. -
Add an
updateListenerto thevalueAnimatorand callemitter.onNextwith the updated animated value. Now whenever thevalueAnimatorobject calls its update listener with a new value, the Observable will emit that value.
Now, add the following line to finish up the reactive wrapper:
return observable.doOnSubscribe { valueAnimator.start() }
You’re returning the observable object you created earlier. You’re using the doOnSubscribe operator to actually start the valueAnimator. Now whenever someone subscribes to the Observable the valueAnimator will start emitting.
Nifty.
Head over to ColorActivity.kt and replace the animateColorChange method with the following:
private fun animateColorChange(newColor: Int) {
val colorFrom = root_layout.background as ColorDrawable
colorAnimator(colorFrom.color, newColor)
.subscribe { color ->
root_layout.setBackgroundColor(color)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
window.statusBarColor = color
}
}
.addTo(disposables)
}
Now, instead of directly using a valueAnimator, you’re using the colorAnimator method you defined earlier and subscribing to the resulting Observable.
Now that this animation is represented via an Observable, you could easily chain together multiple animators and utilize the power of Rx.
Using RxBindings with Android widgets
Now that you’ve react-ified that animation code, it’s time to move on to actually using RxBindings.
First off, open the app’s build.gradle file (not the top-level one!).
Navigate to the dependencies section and take a look at the RxBindings dependencies:
implementation 'com.jakewharton.rxbinding4:rxbinding:4.0.0'
Open ColorViewModel.kt class. If you look at the structure of the class, you’ll notice that there’s two distinct “sections” to the class:
- There’s the
initblock, which is where the bulk of the actual logic is. This is where all of the Rx magic happens. It’s declarative and simple to read. It’s not stateful, and it’s cohesive. - There’s the several
xClickedmethods below theinitblock. These methods are primarily boilerplate to forward relevant data into thehexStringSubject.
If you think about it, these xClicked methods are acting as intermediaries for a stream-like flow. Take the clearClicked method for example. At any point in time, the app is listening for the user to click the Clear button. That event then triggers a call to clearClicked, which then maps that Clear-clicked event into a new value for the hexStringSubject.
Converting clearClicked() to use RxBindings
The clearClicked method is really just an impediment to the above flow. What the app really needs is another Observable<Unit> that represents the user clicking the Clear button — RxBindings provides that functionality, but there’s a catch.
Update the ColorViewModel class header to add a clearStream Observable:
class ColorViewModel(
backgroundScheduler: Scheduler,
mainScheduler: Scheduler,
colorCoordinator: ColorCoordinator,
clearStream: Observable<Unit>
) : ViewModel() { ... }
Now, the ColorViewModel takes a clearStream object of type Observable<Unit>. The type of clearStream is Unit because the app doesn’t actually care what the value emitted by the Observable is — all it cares about is that the Observable emitted something.
Head over to ColorActivity. If you look at the line declaring viewModel, you’ll see that the ColorViewmodel is being instantiated inside a ViewModelProvider.NewInstanceFactory object. There’s a fair amount of boilerplate here that isn’t important. Update the actual line returning a ColorViewModel:
return ColorViewModel(
Schedulers.io(),
AndroidSchedulers.mainThread(),
ColorCoordinator(),
clear.clicks()
) as T
There’s only one thing different here: You’re passing one extra argument — clear.clicks(). clear is a reference to the big X button that clears the current color in the app. The app is using the Kotlin Android Extensions plugin to automatically generate view references, so no more findViewById boilerplate. clicks() is an extension method on View provided by RxBindings that turns a views click listener into an Observable<Unit>. It’s that easy to get an Observable of click events using RxBindings. Isn’t that magical?!
Remove the block of code setting a click listener on the clear view. Since you’ll handle clear events via an Observable, you don’t need to worry about setting a click listener on it anymore.
Back in ColorViewModel, you can also remove the clearClicked method. Again, you’ll be handling clear events via an Observable, so it’s unnecessary.
Now that all the plumbing is in place, it’s time to actually utilize the clearStream to clear out the current color.
In the bottom of the init method, add the following:
clearStream
.map { "#" }
.subscribe(hexStringSubject::onNext)
.addTo(disposables)
The code is dead simple: You’re subscribing to the clearStream Observable and mapping each Unit event to the "#" string. Then you’re forwarding that string to the hexStringSubject object using its onNext method. You’re using a method reference to make the code nice and compact.
Notice that this code is almost identical to the code in clearClicked. Nothing is fundamentally changing, you’re just consolidating the code and Rx-ifying the app!
Run the app. Enter your favorite color string (I know you’ve got favorites!) and click the clear button. It should be cleared.
Dangerzone!
There’s actually a subtle but devious bug in the code you just wrote. To demonstrate the bug, run the app and then rotate the device. Input a hex string and hit the clear button. You’ll notice that nothing happens - the color isn’t cleared.
RxBindings works by generating a series of convenient helper functions on a plethora of different Views. However, when the app is rotated the actual View is destroyed and a new set of Views are created. But the ViewModel you’re using survives the configuration change - that’s the whole point of the ViewModel class!
That means that ColorViewModel is now holding onto a reference for an Observable that’s firing for a View that no longer exists! Since the old clear button has been destroyed and there’s a brand new clear button, RxBindings doesn’t have a reference to the new button. That means you won’t get any of the click effects that you’d expect.
Working around the issue
You can’t just pass in an Observable generated by RxBindings into your ViewModel via the constructor, but you can emulate that reactive flow.
First, you need to go back a few steps. Remove clearStream: Observable<Unit> from the ColorViewModel constructor and in ColorActivity, delete clear.clicks() from the parameter list when creating the viewModel object.
Then, start fixing the problem by adding a new property to the top of ColorViewModel:
private val clearStream = PublishSubject.create<Unit>()
Now add back in the clearClicked method and trigger the new clearStream subject:
fun clearClicked() = clearStream.onNext(Unit)
You’re now building up your own clear clicked Observable without the bugs discovered earlier.
The last thing you need to do is trigger the clearClicked method. You can use RxBindings in your ColorActivity to keep things nice and reactive. Add the following below the viewModel creation block:
clear.clicks().subscribe { viewModel.clearClicked() }
.addTo(disposables)
You’re now getting all of the benefits of reactive views without the bug previously discovered.
Converting backClicked() to use RxBindings
Now that you’ve handled the Clear button, you’re going to go through the same process for the Back button. Update the ColorViewModel with another PublishSubject to represent back clicks:
private val backStream = PublishSubject.create<Unit>()
And update the backClicked method to forward a value into backStream
fun backClicked() = backStream.onNext(Unit)
Finally, start listening to actual click events on the back button with RxBindings in ColorActivity:
back.clicks().subscribe { viewModel.backClicked() }
.addTo(disposables)
Take a look at the old implementation of the backClicked method:
fun backClicked() {
if (currentHexValue().length >= 2) {
hexStringSubject.onNext(currentHexValue()
.substring(0, currentHexValue().lastIndex))
}
}
This method is a bit more complicated than the clearClicked method you replaced earlier. If the current hex value has a length greater than two — i.e., it’s more than just the "#" string — then you want to add a new string onto the hexStringSubject. That new string is whatever the current string is minus the last character.
Add the following code at the bottom of the classes init block in ColorViewModel:
// 1
backStream
// 2
.map { currentHexValue() }
// 3
.filter { it.length >= 2 }
// 4
.map { it.substring(0, currentHexValue().lastIndex) }
// 5
.subscribe(hexStringSubject::onNext)
.addTo(disposables)
Reactive programming is so different from imperative programming that it may be a good idea to break the above code down:
- You’re subscribing to the
backStreamObservable, which is anObservable<Unit>. Every time the user clicks the Back button, this Observable will emit aUnitvalue. - You don’t actually care about the
Unitvalue emitted by the Observable. You’re just using it as a trigger. So you’re immediately mapping thatUnitobject to thecurrentHexValue(), which you’ll use later on in the stream. - You only want to proceed through the chain if the current hex values length is greater than or equal to two.
- Next up you’re getting a substring of the current hex value starting at zero and going up to, but not including, whatever the last index of the hexstring is. The last index is just the size of the string minus 1. Since you used the
filteroperator above, you can be confident that this string will have a length >= 2. - You’re subscribing to the Observable and forwarding the emitted string to the
hexStringSubject, so the rest of the code above this block can react to the new hex string value.
Note: You may be tempted to avoid the first
mapcall and instead just operate directly on whatevercurrentHexValue()provides in both thefilteroperator and the secondmapoperator. While that may be tempting, it would also introduce a race condition and a potential crash! Between the firstfilterbeing executed and the secondmapbeing executed, another thread could update the current hex value and your assumption that the length of the string returned bycurrentHexValue()being greater than two is no longer certain. Chances are it wouldn’t happen in this app, but it’s always worth keeping those potential race conditions in mind.
Boom! You’ve replicated the code in backClicked in a more streamlined reactive style. Run the app. Tapping the Back key should work exactly the same.
Last but not least is the digitClicked method.
Converting digitClicked() to use RxBindings
Again, add a new subject representing digit clicks in the ColorViewModel class:
private val digitsStream = BehaviorSubject.create<String>()
This time you’re using a BehaviorSubject so that every time you subscribe to the stream you’ll get the latest and greatest digit clicks.
The incoming digits will be of type String. Each String will be a single character.
In ColorActivity, delete the line declaring digits and the forEach block setting click listeners on each digit.
Then, replece the digits declaration and the following digits.forEach with the following snippet:
// 1
val digits = listOf(zero, one, two, three, four, five, six,
seven, eight, nine, A, B, C, D, E, F)
// 2
.map { digit ->
// 3
digit.clicks().map { digit.text.toString() }
}
If the above is confusing, don’t worry! Here’s a breakdown:
- Build up a list of each digit on the “keypad” in the app. Each object in this list is a
TextView. Again, the app is using the Kotlin Android Extensions to provide easy reference to each view in the app. So thislistOf()call returns aList<TextView>. - Call
mapon this list.digitin the lambda block is aTextView. - Call
clicks()on eachdigitto turn it into anObservable<Unit>. Then, call amapon that Observable. Map theUnitvalue to the string representation of the text in thedigitTextView. Don’t be confused by the two maps — one is on theList<TextView>, the other is on theObservable<Unit>.
The result of the above code is that digits is now a List<Observable<String>>. How meta is that?!
Now, add the following below digits:
val digitStreams = Observable.merge(digits)
You’re using the merge method you learned about in Chapter 9, “Combining Operators,” to combine the List<Observable<String>> into a single Observable<String>. Now, any time a user taps one of the digits in HexColor digitStreams will emit.
Next, update the digitClicked method to forward your digit through to your subject:
fun digitClicked(digit: String) = digitsStream.onNext(digit)
Finally, subscribe to the digitStream stream you just created in ColorActivity and forward the result through to the ColorViewModel:
digitStreams.subscribe(viewModel::digitClicked)
.addTo(disposables)
You’re using a method reference to make the code even more concise.
Back in ColorViewModel, add the following at the bottom of the init file:
digitsStream
// 1
.map { it to currentHexValue() }
// 2
.filter { it.second.length < 7 }
// 3
.map { it.second + it.first }
.subscribe(hexStringSubject::onNext)
.addTo(disposables)
Here’s another breakdown:
- Take the
Stringemitted bydigitsStreamand usemapto combine it with whatever the current hex value is. Thetoinfix function is a simple shorthand to create aPairobject. - Use
filterto ignore any element emitted while the current hex values length is ≥ 7. If the current hex value is ≥ 7, you want to ignore any taps, since the full hex color string has already been input. - Now, combine the current hex value and the
Stringthe user just tapped, appending the new string onto the existing hex value.
Delete the old digitClicked method and run the app.
Now, take a step back and look at the ColorViewModel class. Doesn’t it look fantastic? So sleek and declarative. Just a real beauty. Don’t you wish all code could be this declarative and reactive?
Fetching colors from an API
The code for the app is looking a lot better. But the app itself is still fairly limited; it can only display names for a small list of colors. You’re going to change that by integrating with the color API found here: www.thecolorapi.com.
Open the ColorService class and add the following method to the bottom of the interface outside the companion object:
@GET("id")
fun getColor(@Query("hex") hex: String): Single<ColorResponse>
The above code will fetch a lot of metadata for a color based off the hex string passed in.
Now add another constructor parameter at the bottom of the list of constructor parameters for the ColorViewModel class:
colorApi: ColorApi
In the ColorActivity, pass in ColorApi to the ColorViewModel constructor:
return ColorViewModel(
Schedulers.io(),
AndroidSchedulers.mainThread(),
ColorCoordinator(),
ColorApi
) as T
Now, open up ColorApi.kt. ColorApi is a simple object that wraps the ColorService class to make API calls. Replace the body of the getClosestColor method with the following:
return colorService.getColor(hexString)
Now, head back to ColorViewModel.kt. It’s time to actually use the API.
Find the Observable chain that searches through the ColorName enum whenever a new hex string comes in. It looks like this:
hexStringSubject
.subscribeOn(backgroundScheduler)
.observeOn(mainScheduler)
.filter { hexString -> ColorName.values()
.map { it.hex }
.contains(hexString) }
.map { hexString -> ColorName.values()
.first { it.hex == hexString } }
.map { it.toString() }
.subscribe(colorNameLiveData::postValue)
.addTo(disposables)
Delete the whole chain and replace it with the following:
hexStringSubject
.filter { it.length == 7 }
.observeOn(mainScheduler)
.flatMapSingle {
colorApi.getClosestColor(it)
.subscribeOn(backgroundScheduler)
}
.map { it.name.value }
.subscribe(colorNameLiveData::postValue)
.addTo(disposables)
You’re again filtering out any hex strings that aren’t yet at size seven. You’re then using flatMap to fetch the color details from the color API making sure to subscribe to the network call off the main thread. Then you’re using map to convert the ColorResponse object you get back from the API into a human readable color name. Finally you’re posting that value to the colorNameLiveData.
Run the app. Try out any color combination, and you’ll see a name. How cool is that?
My favorite is #555555. It makes me feel like a boss.
Displaying an information dialog
Next up on the docket is to allow the user to manually type out a color string without tapping the digits on the app. To do this, you’re going to expose a bottom sheet dialog that includes an EditText widget that the user can input text in.
Add the following to the bottom of the onCreate method in ColorActivity:
color_name.clicks()
.subscribe {
val bottomSheetDialog =
ColorBottomSheet.newInstance(hex.text.toString())
bottomSheetDialog
.show(supportFragmentManager, "Custom Bottom Sheet")
}
.addTo(disposables)
You’re using the clicks extension method on the color_name widget to create an Observable<Unit> representing clicks. In the subscribe block you’re creating an instance of the ColorBottomSheet fragment with the current hex color string value and showing it.
Run the app and input a color. If you click on the color name in the top-right, you should see a bottom dialog with an empty EditText appear.
Open up the ColorBottomSheetViewModel class. It’s pretty empty right now. It has three LiveData objects — one for showing a loading indicator, one for showing the name of the color input by the user, and a last one for showing the “closest” matching color to whatever color the user inputs.
Note: The concept of a difference or distance between two colors is actually really interesting! You can use the distance formula you learned in algebra to get the “distance” between two colors. Wikipedia has a great article about it: https://en.wikipedia.org/wiki/Color_difference.
Before you can start using the color API in this new bottom sheet you need to access the string that the user types into the EditText at the top of the bottom sheet.
Normally, the way you’d listen for text changes on an EditText would be to create a new TextWatcher object and implement the afterTextChanged event. But that’s a lot of boilerplate. You’ll use RxBindings instead to get an Observable<String> representing the text changes.
First, update the ColorBottomSheetViewModel to have a val representing search strings. Add the following to the top of the class:
private val searchObservable = BehaviorSubject.create<String>()
And just like before add a method that will be called to forward a value into your new Observable:
fun onTextChange(text: String) = searchObservable.onNext(text)
Next, head to ColorBottomSheet and add the following below the line declaring the viewModel in onViewCreated:
hex_input.textChanges()
.map { it.toString() }
.subscribe { viewModel.onTextChange(it) }
textChanges is an RxBindings extension method on TextView. It turns an Observable<Editable>, so you’re using map to convert the Editable into a String. You’re then subscribing to the resulting Observable and forwarding it through to your ViewModel.
Since you’re using the subscribe method, you need to make sure to dispose of the resulting Disposable at some point. Add a CompositeDisposable value at the top of the file:
private val disposables = CompositeDisposable()
Update the textChanges block you just wrote to add the Disposable into your newly created disposables object:
hex_input.textChanges()
.map { it.toString() }
.subscribe { viewModel.onTextChange(it) }
.addTo(disposables)
You’re now ready to use the text-changes Observable to fetch a color from the color API.
Head back to ColorBottomSheetViewModel and create an init block with the following code:
init {
val colorObservable = searchObservable
.filter { it.length == 7 }
.flatMapSingle {
ColorApi.getClosestColor(it).subscribeOn(Schedulers.io())
}
.map { it.name }
.share()
}
Above:
- You’re using the
Observable<String>passed in fromColorBottomSheet, which you got via the RxBindings library, to construct a new Observable chain. It first filters out any inputs that aren’t of length seven, since that’s the correct length for a hex color string. - Then you’re using
flatMapSingleto fetch the closest color to the input string from the color api, which returns aSinglewith the network response. - Finally, you’re using
mapto pull out theColorNameobject from theColorResponse. - You’re using the
shareoperator to share the whole chain so you can have multiple subscribers listen to the same set of results.
The above block is great, but you’re still not actually subscribing to the colorObservable or emitting anything into the LiveData objects. Add the following below the colorObservable chain:
colorObservable
.subscribe { colorNameLiveData.postValue(it.value) }
.addTo(disposables)
That’s more like it! You’re subscribing to colorObservable and pulling out the value object from the ColorName class, which maps to the name of the color, and pushing it through the colorNameLiveData object.
Before you run the app, finish off the functionality of the bottom sheet by adding one more subscriber:
colorObservable
.subscribe {
closestColorLiveData.postValue(it.closest_named_hex)
}
.addTo(disposables)
You’re now pulling out the closest_named_hex value from the ColorName object and pushing it through the closestColorLiveData object. Nice!
Note:
closest_named_hexis named with underscores rather than the normal camelCase format to allow for automagical json deserializing from theGSONdeserialization library.
Run the app, enter a color and tap the color name to show the bottom sheet. Input a hex value and you should see the name of the closest named color the API could find, as well as the hex value of that closest color. You may need to close the keyboard to see the output.
The app is almost perfect. The only issue is if you enter a value on the main screen and then click the color name you don’t see the details of the color that you input. You only see the closest color and the name of that color if you type in something new. This is an easy one line fix.
In ColorBottomSheetViewModel utilize the startsWithItem operator before the filter operator in the colorObservable declaration (first chain):
.startWithItem(startingColor)
Now, run the app and tap around. You should see the details of whatever color you originally input on the bottom sheet when it first appears.
Challenges
Challenge 1
Start from the final project from this chapter and update the bottom color sheet to show a loading indicator while the ColorBottomSheetViewModel is loading a color from the color API.
You don’t need to worry about adding a new view or hooking up a new live data. You can use the showLoadingLiveData to toggle whether the loading indicator should be shown or hidden.
Challenge 2
Update the ColorBottomSheet so that the EditText input always includes a # character, is limited to seven characters, and only allows characters between 1-9 and A-F.
To accomplish this challenge, you’ll want to use another RxBindings method on EditText, specifically the afterTextChangesEvents method. afterTextChangesEvents produces an Observable<TextViewAfterTextChangeEvent>. TextViewAfterTextChangeEvent includes an Editable object that you can manipulate to only include the strings that you want.
If you’re having difficulties, take a look at the completed challenges project for a hint.
Key points
- Practicing creating reactive extensions around existing Android classes.
- Using the RxBindings library to create reactive streams from Android widgets.
- Using the
clicksextension method to replace an Android click listener. - Using the
textChangesextension method to get a stream ofTextVieworEditTextchanges. - Using the
afterTextChangeEventsmethod to get a stream describing any changes that are happening to anEditText.
Where to go from here?
If you’re hooked on RxJava, RxBindings is a great supplement to the regular classes. RxBinding is simple to use, provides a consistent API for consumption, and makes your application much more composable and reactive.