Chapters

Hide chapters

Reactive Programming with Kotlin

Second Edition · Android 10 · Kotlin 1.3 · Android Studio 4.0

Before You Begin

Section 0: 3 chapters
Show chapters Hide chapters

Section II: Operators & Best Practices

Section 2: 7 chapters
Show chapters Hide chapters

17. RxBindings
Written by Alex Sullivan

Heads up... You're reading this book for free, with parts of this chapter shown beyond this point as scrambled text.

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.

fun colorAnimator(fromColor: Int, toColor: Int): Observable<Int> {
  return Observable.empty()
}
// 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)
  }
}
return observable.doOnSubscribe { valueAnimator.start() }
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)
}

Using RxBindings with Android widgets

Now that you’ve react-ified that animation code, it’s time to move on to actually using RxBindings.

implementation 'com.jakewharton.rxbinding4:rxbinding:4.0.0'

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.

class ColorViewModel(
  backgroundScheduler: Scheduler,
  mainScheduler: Scheduler,
  colorCoordinator: ColorCoordinator,
  clearStream: Observable<Unit>
) : ViewModel() { ... }
return ColorViewModel(
  Schedulers.io(),
  AndroidSchedulers.mainThread(),
  ColorCoordinator(),
  clear.clicks()
) as T
clearStream
  .map { "#" }
  .subscribe(hexStringSubject::onNext)
  .addTo(disposables)

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.

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.

private val clearStream = PublishSubject.create<Unit>()
fun clearClicked() = clearStream.onNext(Unit)
clear.clicks().subscribe { viewModel.clearClicked() }
    .addTo(disposables)

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>()
fun backClicked() = backStream.onNext(Unit)
back.clicks().subscribe { viewModel.backClicked() }
    .addTo(disposables)
fun backClicked() {
    if (currentHexValue().length >= 2) {
        hexStringSubject.onNext(currentHexValue()
            .substring(0, currentHexValue().lastIndex))
    }
}
// 1
backStream
  // 2
  .map { currentHexValue() }
  // 3
  .filter { it.length >= 2 }
  // 4
  .map { it.substring(0, currentHexValue().lastIndex) }
  // 5
  .subscribe(hexStringSubject::onNext)
  .addTo(disposables)

Converting digitClicked() to use RxBindings

Again, add a new subject representing digit clicks in the ColorViewModel class:

private val digitsStream = BehaviorSubject.create<String>()
// 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() }
  }
val digitStreams = Observable.merge(digits)

fun digitClicked(digit: String) = digitsStream.onNext(digit)
digitStreams.subscribe(viewModel::digitClicked)
    .addTo(disposables)
digitsStream
  // 1
  .map { it to currentHexValue() }
  // 2
  .filter { it.second.length < 7 }
  // 3
  .map { it.second + it.first }
  .subscribe(hexStringSubject::onNext)
  .addTo(disposables)

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.

@GET("id")
fun getColor(@Query("hex") hex: String): Single<ColorResponse>
colorApi: ColorApi
return ColorViewModel(
  Schedulers.io(), 
  AndroidSchedulers.mainThread(),
  ColorCoordinator(),
  ColorApi
) as T
return colorService.getColor(hexString)
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)
hexStringSubject
  .filter { it.length == 7 }
  .observeOn(mainScheduler)
  .flatMapSingle {
    colorApi.getClosestColor(it)
        .subscribeOn(backgroundScheduler)
  }
  .map { it.name.value }
  .subscribe(colorNameLiveData::postValue)
  .addTo(disposables)

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.

color_name.clicks()
  .subscribe {
    val bottomSheetDialog =
      ColorBottomSheet.newInstance(hex.text.toString())
    bottomSheetDialog
        .show(supportFragmentManager, "Custom Bottom Sheet")
  }
  .addTo(disposables)

private val searchObservable = BehaviorSubject.create<String>()
fun onTextChange(text: String) = searchObservable.onNext(text)

hex_input.textChanges()
  .map { it.toString() }
  .subscribe { viewModel.onTextChange(it) }
private val disposables = CompositeDisposable()
hex_input.textChanges()
    .map { it.toString() }
    .subscribe { viewModel.onTextChange(it) }
    .addTo(disposables)
init {
  val colorObservable = searchObservable
    .filter { it.length == 7 }
    .flatMapSingle {
      ColorApi.getClosestColor(it).subscribeOn(Schedulers.io())
    }
    .map { it.name }
    .share()
}
colorObservable
  .subscribe { colorNameLiveData.postValue(it.value) }
  .addTo(disposables)
colorObservable
  .subscribe {
    closestColorLiveData.postValue(it.closest_named_hex)
  }
  .addTo(disposables)

.startWithItem(startingColor)

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.

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.

Key points

  • Practicing creating reactive extensions around existing Android classes.
  • Using the RxBindings library to create reactive streams from Android widgets.
  • Using the clicks extension method to replace an Android click listener.
  • Using the textChanges extension method to get a stream of TextView or EditText changes.
  • Using the afterTextChangeEvents method to get a stream describing any changes that are happening to an EditText.

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.

Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.
© 2024 Kodeco Inc.

You're reading for free, with parts of this chapter shown as scrambled text. Unlock this book, and our entire catalogue of books and videos, with a Kodeco Personal Plan.

Unlock now