6.
Filtering Operators in Practice
Written by Alex Sullivan & Marin Todorov
In the previous chapter, you began your introduction to the functional aspect of RxJava. The first batch of operators you learned about helped you filter the elements of an observable sequence. As explained previously, the operators are simply methods on Observable and other associated RxJava types.
The operators operate on the elements of their Observable class and produce a new observable sequence as a result. This comes in handy because, as you saw previously, this allows you to chain operators, one after another, and perform several transformations in sequence:
The preceding diagram looks great in theory. In this chapter, you’re going to try using the filtering operators in a real-life app. In fact, you are going to continue working on the Combinestagram app that you already know and love from Chapter 4, “Observables & Subjects in Practice”.
Note: In this chapter, you will need to understand the theory behind the filtering operators in RxJava. If you haven’t worked through Chapter 5, “Filtering Operators,” do that first and then come back to the current chapter.
Improving the Combinestagram project
In this chapter, you will:
- Work through series of tasks, which (surprise!) will require you to use various filtering operators.
- Use different ones and see how you can use counterparts like
skipandtake. - Take care of a few of the issues in the current Combinestagram project.
Note: Since this book has only covered a few operators so far, you will not write the “best possible” code. For this chapter, don’t worry about best practices or proper architecture yet, but instead focus on truly understanding how to use the filtering operators. In this book, you’re going to slowly build up towards writing good RxJava code. It’s a process!
Refining the photos sequence
Currently, the main screen of the app looks like this:
Right now, the app works by opening up an instance of PhotosBottomDialogFragment whenever the user clicks the add button. Then, when the user clicks one of the photos, a photo object is added to the selectedPhotosSubject publish subject. The SharedViewModel subscribes to the selectedPhotosSubject and adds the newly emitted photo object onto its own imagesSubject. Whenever imagesSubject emits, the selectedPhotos live data object is updated and the MainActivity class receives the new photo.
That’s all well and good, but Combinestagram could use a few new features. For example, wouldn’t it be nice if you could view a thumbnail of the image collage? I’ll answer that for you. It’d be great!
You could just add more code to the subscribe block in the subscribeSelectedPhotos method, but that would be messy and that subscribe block will quickly become too complex if you go that route.
Another option would be to create another subscription to the selectedPhotos observable. That would actually work here, but there’s an important consideration to make before you go down that path.
Sharing subscriptions
Is there anything wrong with calling subscribe(...) on the same observable multiple times? Turns out there might be!
You’ve already seen that observables are lazy, pull-driven sequences. Simply calling a bunch of operators on an Observable doesn’t involve any actual work. The moment you call subscribe(...) directly on an observable or on one of the operators applied to it, that’s when the Observable livens up and starts producing elements.
Take a look at the code below:
val numbers = Observable.create<Int> { emitter ->
val start = getStartNumber()
emitter.onNext(start)
emitter.onNext(start + 1)
emitter.onNext(start + 2)
emitter.onComplete()
}
The code creates an Observable<Int>, which produces a sequence of three numbers: start, start + 1, start + 2.
Now see what getStartNumber() looks like:
var start = 0
private fun getStartNumber(): Int {
start++
return start
}
The function increments a variable and returns it; nothing can go wrong there. Or can it? Add a subscription to numbers in one of the earlier IntelliJ projects and see for yourself:
numbers
.subscribeBy(
onNext = { println("element [$it]") },
onComplete = { println(("-------------"))}
))
You will get the exact output you expected. Yay!
element [1]
element [2]
element [3]
-------------
Copy and paste the exact same subscription code one more time though, and this time the output is different.
element [1]
element [2]
element [3]
-------------
element [2]
element [3]
element [4]
-------------
The problem is that each time you call subscribe(...), this creates a new Observable for that subscription — and each copy is not guaranteed to be the same as the previous. And even when the Observable does produce the same sequence of elements, it’s overkill to produce those same duplicate elements for each subscription. Imagine if your observable wraps a network call - by subscribing twice, you’d end up making that same network call twice. Wasteful!
It’s worth noting that Subjects don’t have this problem - since every subscriber will get new items as they’re emitted (depending on the subject type) you don’t need to worry about the initial work done in the create block being repeated.
To share a subscription, you can use the share() operator. A common pattern in Rx code is to create several sequences from the same source Observable by filtering out different elements in each of the results.
You’ll use share in a practical example in Combinestagram to understand its purpose a bit better.
Open the project and select SharedViewModel. Scroll to subscribeSelectedPhotos() and add this line as the first line in the method:
val newPhotos = fragment.selectedPhotos.share()
Then, instead of subscribing to the fragment.selectedPhotos observable, subscribe to the newPhotos observable:
subscriptions.add(newPhotos
.doOnComplete {
Log.v("SharedViewModel", "Completed selecting photos")
}
.subscribe { photo ->
imagesSubject.value?.add(photo)
imagesSubject.onNext(imagesSubject.value ?:
mutableListOf())
}
)
It’s no longer true that each subscription is creating a new Observable instance like this:
Instead, with share(), you allow for multiple subscriptions to consume the elements that a single Observable produces for all of them, like so:
Now you can create a second subscription to newPhotos and filter out some of the elements you don’t need.
Before moving on though, it’s important to learn a bit more about how share works.
share creates a subscription only when the number of subscribers goes from 0 to 1 (e.g., when there isn’t a shared subscription already). When a second, third and so on subscriber starts observing the sequence, share uses the already created subscription to share with them. If all subscriptions to the shared sequence get disposed (e.g. there are no more subscribers), share will dispose the shared sequence as well. If another subscriber starts observing, share will create a new subscription for it just like described above.
Note:
share()does not provide any of the subscriptions with values emitted before the subscription takes effect.
The rule of thumb about sharing operators is that it’s safe to use share() with observables that do not complete, or if you guarantee no new subscriptions will be made after completion.
Ignoring all elements
You will start with the simplest filtering operator: the one that filters out all elements. No matter your value or type, ignoreElements() says “You shall not pass!”
Recall that newPhotos emits a Photo element each time the user selects a photo. In this section, you are going to add a small thumbnail of the collage in the middle of the screen.
Since you would like to update that icon only once, when the user dismisses the photo dialog fragment, you need to ignore all Photo elements and act only on a completed event.
ignoreElements() is the operator that lets you do just that: it discards all elements of the source sequence and lets through only complete or error.
Inside subscribeSelectedPhotos() at the bottom of the method, add the following:
subscriptions.add(newPhotos
.ignoreElements()
.subscribe {
})
Before you flesh out the subscribe block you need to create a new enum class to represent the thumbnail status.
Add a new file called ThumbnailStatus.kt.
Add the following to the new file:
enum class ThumbnailStatus {
READY,
ERROR
}
A thumbnail can be READY, or something may have gone wrong, so it might be in an ERROR state.
Now add a new MutableLiveData variable that will notify the MainActivity to update the thumbnail image. Add the following val to the top of SharedViewModel:
private val thumbnailStatus = MutableLiveData<ThumbnailStatus>()
And add a corresponding getter:
fun getThumbnailStatus(): LiveData<ThumbnailStatus> {
return thumbnailStatus
}
Finally, head back to the subscribeSelectedPhotos method and finish up the empty new subscribe block you added earlier:
subscriptions.add(newPhotos
.ignoreElements()
.subscribe {
thumbnailStatus.postValue(ThumbnailStatus.READY)
}
This subscription to newPhotos will ignore all images and will run the subscribe lambda when the user returns to the main activity.
Last but not least, you need to actually consume the new thumbnailStatus live data object in MainActivity. Add the following to the bottom of the onCreate method:
viewModel.getThumbnailStatus().observe(this,
Observer { status ->
if (status == ThumbnailStatus.READY) {
thumbnail.setImageDrawable(collageImage.drawable)
}
}
)
If the thumbnail status is READY, the activity will update the thumbnail ImageView with whatever image is in the collageImage ImageView.
Run the app. Whenever you come back from selecting a photo, you should see the thumbnail box updated with the current collage:
Filtering elements you don’t need
Of course, as great as ignoreElements() is, sometimes you will need to ignore just some of the elements — not all of them.
In those cases, you will use filter() to let some elements through and discard others.
For example, you might have noticed that photos in portrait orientation do not fit very well in the collages in Combinestagram.
Of course, you could write smarter collage-building code… but in this chapter you’re going to discard portrait photos and only include landscapes instead. That’s one way to solve the issue. Pretend it’s a feature, and not a bug!
Scroll to the top of subscribeSelectedPhotos and then add following operator after the doOnComplete call:
subscriptions.add(newPhotos
.doOnComplete {
// ..
}
.filter { newImage ->
val bitmap = BitmapFactory.decodeResource(
fragment.resources, newImage.drawable)
bitmap.width > bitmap.height
}
.subscribe { photo ->
// ..
}
)
Now each photo that newPhotos emits will have to pass a test before it gets to subscribe(...). Your filter operator will check if the width of the image is larger than its height, and if so, it will let it through. Photos in portrait orientation will be discarded.
Run the app and try adding the portrait photo at the bottom of the photo dialog fragment (scroll down if you don’t see it). No matter how many times you tap on the photo in portrait orientation, it will not be added to the collage.
Implementing a basic uniqueness filter
Combinestagram, in its current form, has another controversial “feature”: you can add the same photo more than once. That doesn’t make for very interesting collages, so in this section you’ll add some advanced filtering to prevent the user from adding the same photo multiple times.
Note: There are better ways to achieve the required result than what you are going to implement below. It is, however, a great exercise to build a solution with your current RxJava skill set.
In order to check for duplicate collage images, you need a way to keep track of all the images that have been added so far. Luckily, you’re using a BehaviorSubject with a list of photo objects, and since BehaviorSubject exposes its current value you can just check against that!
Add the following code below the filter operator you just added:
subscriptions.add(newPhotos
.doOnComplete {
// ..
}
.filter { newImage ->
// ..
// 1
}
.filter { newImage ->
// 2
val photos = imagesSubject.value ?: mutableListOf()
// 3
!(photos.map { it.drawable }
// 4
.contains(newImage.drawable))
}
.subscribe { photo ->
// ..
}
)
Here’s a breakdown of the above code:
- You’re again using the
filteroperator to filter out duplicates images - You’re getting the latest list of photos from
imagesSubject. Since aBehaviorSubjectcould be in a state where an initial value hasn’t been supplied,valueis nullable. If you get anullvalue, which you shouldn’t in this app, you’ll instead use an empty list. - Next up you’re calling
mapon the list of photos to turn it into a list ofintvalues. Remember that theintdrawable value on a photo represents a drawable ID that Android can use to fetch a real drawable. - Finally, you’re checking to see if this list of drawable ids contains the new images drawable id. If it does, you return
false, so the filter fails.
Run the app. You won’t be able to add duplicate images anymore.
Keep taking elements while a condition is met
One of the “best” bugs in Combinestagram is that the Add button is disabled if you add six photos, which prevents you from adding any more images. But if you are in the photos bottom dialog fragment, you can add as many as you wish. There ought to be a way to limit those, right?
Well, believe it or not, you can easily filter all elements after a certain condition has been met by using the takeWhile() operator. You provide a boolean condition, and takeWhile() discards all elements when this condition evaluates to false.
Scroll again towards the top of subscribeToSelectedPhotos and add the following operator, again after doOnComplete:
subscriptions.add(newPhotos
.doOnComplete {
// ..
}
.takeWhile {
imagesSubject.value?.size ?: 0 < 6
}
.filter { newImage ->
// ..
}
.filter { newImage ->
// ..
}
.subscribe { photo ->
// ..
}
)
takeWhile(...) will let photos through as long as the total number of images in the collage is less than six. You use the ?: Elvis operator to default to 0 if imagesSubject.value?.size is null.
Run the app and try to add lots of photos to the collage. Once you add six photos, you won’t be able to add any more. Mission accomplished!
Improving the photo selector
One common source of bugs in Android applications is what happens when a user quickly taps on a button multiple times. My guess is you’ve been in an app before where you quickly tapped a button and saw the application display multiple new activities.
A similar bug can happen in Combinestagram. If a user quickly taps two photos, the app will add those two photos. That might have been a mistake from the user’s perspective. Luckily, you can use RxJava to quickly take care of that pesky issue!
Add the following new operator to the observable chain in subscribeToSelectedPhotos right before the actual subscribe call:
.debounce(250, TimeUnit.MILLISECONDS,
AndroidSchedulers.mainThread())
There’s two interesting things happening above. The first is the use of the debounce operator, the second is the use of the AndroidSchedulers.mainThread() call. Schedulers can be complex, but you’ll learn all about them in more detail in a future chapter. For this example all you need to know is that to keep this code executing on the Android main thread you need to pass in the AndroidSchedulers.mainThread() scheduler.
debounce is an extremely handy operator that limits the number of events that get through to your subscribe block. debounce takes in an amount of time, 250 milliseconds in the above example, and makes sure that no new items are emitted until that time window runs out. If a new next event is emitted before that time period elapses, the old item will be dropped and the a new timer will start.
Timing operators can be challenging to understand, so here’s an example.
Imagine you have an observable that emits A after one second, B after another second, and then C after 5 more seconds. If you were to call debounce on that observable and gave it a time period of two seconds you’d only receive two values in your subscribe block. You’d receive B after four seconds and then C after one more second. The A would be dropped since the B value came quickly after it.
Go ahead and run the app again. You’ll find that if you quickly tap two photos only the latest one will be added. Nice!
Challenge
Challenge: Combinestagram’s source code
Your challenge is to notify the user that they’ve reached the photo limit once they add 6 photos. Here’s a few hints on how to proceed with this challenge.
First, you’ll need some way to tell the Activity class that the photo limit has been reached. A good way to signal that information would be to create a new enum class called CollageStatus that could be exposed in a new LiveData instance.
Second, you’ll need some way to figure out what the current CollageStatus is. Luckily you have imagesSubject, which you can subscribe to and check to see if 6 images have been selected. If you’re feeling fancy, you can use the share operator on the imagesSubject to practice your sharing skills. Sharing is caring after all! However, since imagesSubject is a Subject, the share operator is a bit redundant so feel free to skip that step.
Key points
- You can share subscriptions to a single observable using
share(). -
ignoreElementscomes in handy when you want to only look for stop events. - Filtering out elements in an observable using
filterlets you prevent certain elements from coming through the stream, like allowing only landscape and not portrait photos. - Implementing a uniqueness filter can be achieved by combining
filterwith the current value of aBehaviorSubject. -
Debouncing with the
debounceoperator helps you to get around pesky bugs that occur in apps due to rapid user interactions with the interface.
Where to go from here?
You now have a handle on the first type of RxJava operators we’ll examine, filtering operators, and have used them in an Android app.
Next up, you’ll learn about our second type of RxJava operators, transforming operators, which let you modify the data being sent through the observable stream.