Handling search with RxJava2 and Kotlin
by: Somodi Karoly Akos The post first appeared ✍🏼 on the Kotlin Development blog
Presume we have to solve search in an Android app.We just need an EditText and query data based on the input, right? Sounds easy…
Let’s take a look at how would we implement with Kotlin and Reactive Extensions!
Basic solution
Before we jump in and start coding, let’s make sure all the necessary libraries are added to the Gradle file. Also, we’ll add a simple EditText to the activity layout, and subscribe to the textChanges observable. Up until this point, this looks like the simplest task ever.
build.gradle(module.app)
// RxJava2
compile "io.reactivex.rxjava2:rxjava:2.1.2"
compile "io.reactivex.rxjava2:rxandroid:2.0.1"
// RxBindig
compile "com.jakewharton.rxbinding2:rxbinding:2.0.0"
compile "com.jakewharton.rxbinding2:rxbinding-kotlin:2.0.0"
activity_layout.xml
<android.support.v7.widget.AppCompatEditText
android:id="@+id/mainSearchText"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
onCreate subscription
RxTextView.textChanges(mainSearchText)
.subscribe(
{ Log.d("MainActivity", "$it") },
{ Log.e("MainActivity", "$it") }
)
Kotlin ❤ You
Why are we using the RxTextView utility class to observe the text change events? Can’t we apply some Kotlin magic to make our life easier? Maybe Jake Wharton asked the same question when he made the RxBinding Kotlin module. It adds extension functions to the view types, making it easier for the IDE to suggest possible functions.
mainSearchText
.textChanges()
.subscribe({
Log.d("MainActivity", it.toString())
}, {
Log.e("MainActivity", it.toString())
})
One can easily see how gorgeous the whole stream is without the anonymous classes, thanks to Kotlin language features and lambda syntax. Less code means less time spent on maintenance.
Let’s upgrade the UI
We’ll add a couple of shiny and good looking features to our layout. Add two drawables into the EditText, configure the keyboard to display a search icon, add a hint inside the input field, and a progress bar above the content. It’s super important to indicate progress, because without these UI feedback, the users wouldn’t know that they need to be patient.
Extended EditText
<android.support.v7.widget.AppCompatEditText
android:id="@+id/mainSearchText"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginEnd="16dp"
android:background="@android:color/transparent"
android:drawableEnd="@drawable/main_close_white_24dp"
android:drawableStart="@drawable/main_search_white_24dp"
android:hint="@string/main_search_hint"
android:imeOptions="actionSearch"
android:inputType="text"
android:lines="1"
android:maxLines="1"/>
Content body
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent"> <TextView
android:id="@+id/mainContent"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:gravity="center"
android:textColor="@color/colorAccent"
android:textSize="24sp"
android:textStyle="bold"/> <ProgressBar
android:id="@+id/mainProgressbar"
android:layout_width="96dp"
android:layout_height="96dp"
android:layout_gravity="center"
android:indeterminate="true"
android:visibility="gone"/>
</FrameLayout>
As an extra, let’s add an extension function to the EditText which will handle the onTouch event of the drawable on the right, so we can clear our input based on a touch of the right drawable if it is clicked.
Extension function for EditText
fun AppCompatEditText.setRightDrawableOnTouchListener(func: AppCompatEditText.() -> Unit) {
setOnTouchListener { _, event ->
var consumed = false
if (event.action == MotionEvent.ACTION_UP) {
val drawable = compoundDrawables[2]
if (event.rawX >= (right - drawable.bounds.width())) {
func()
consumed = true
}
}
consumed
}
}
Handling the ‘clear’ touch event
mainSearchText.setRightDrawableOnTouchListener {
text.clear()
}
To the internet and beyond
Until now we did nothing of the extreme, but here comes true magic. Let’s connect our search EditText to a query service — it can be anything from a database to a server, depending on the requirements.
.flatMap { searchService.search(it).subscribeOn(Schedulers.io()) }
.observeOn(AndroidSchedulers.mainThread())
We’re doing work on the IO thread as specified by subscribeOn
, but at the end of the day we need to mutate the state of views based on the results. This is what the observeOn
method specifies, we select the thread which will deal with the subscription and the side effects.
Showing that the app is working in the background is always a good idea, so we’re displaying a progress bar until our work is finished, with the doOnNext
and doOnEach
functions.
before flatMap
.doOnNext {
mainProgressbar.visibility = View.VISIBLE
mainContent.visibility = View.GONE
}
after observeOn
.doOnEach {
mainProgressbar.visibility = View.GONE
mainContent.visibility = View.VISIBLE
}
As a finishing touch, we’ll add an extra line to our subscription mainContent.text = it.text
and the result will be displayed on the UI. Beautiful, isn't it?
Where we are going, we don’t need error handling
If something can go wrong, it will — a good developer always keeps an eye out for possible errors.
We handled visibility in doOnEach
, so no matter if the stream throws an exception, or finishes correctly, the progress bar will be hidden, and the content will be visible.
Every time something subscribes to the EditText, the content will be emitted immediately. To avoid this behavior, simply skip the first element:
.skip(1)
If you look at the server/database log, you will see that we are doing a query each time the user hits a character, meaning we’re burning CPU and network resources when we wouldn’t have to. Add a little debounce timer between doOnNext
and flatMap
to avoid this.
.debounce(800, TimeUnit.MILLISECONDS)
Moving on to the query, what if it fails? What if our service can’t process the search phrase? onError
will be triggered, making the app unsubscribe from textChanges()
, and the EditText will no longer be emitting anything. Sounds bad, right? Just because a request is not finished properly, I don't want to lose my UI logic inside my activity. We’ll inform our users about errors with doOnError
, and use theretry
to resubscribe to the textChanges
observable.
.doOnError {
Snackbar.make(main_coordinator, "Error while searching", Snackbar.LENGTH_SHORT).show()
}
.retry()
RxBinding knowledge
If we’ve read the documentations before implementing anything — what we always do before starting to write code — we would saw the warning for the textChange event:
Create an observable of character sequences for text changes on View
Warning: Values emitted by this observable are mutable and owned by the host
That means every value passed down the stream can be changed by the system if the value is delayed or debounced on an another thread. Which we definitely do, so to avoid mutability simply map the value to a new object after skip
.map { it.toString() }
Final code
See the final version here:
searchSubscription = mainSearchText
.textChanges()
.skip(1)
.map { it.toString() }
.doOnNext {
mainProgressbar.visibility = View.VISIBLE
mainContent.visibility = View.GONE
}
.debounce(800, TimeUnit.MILLISECONDS)
.flatMap {
if (it.isNotBlank()) {
searchService.search(it).subscribeOn(Schedulers.io())
} else {
Observable.just(SearchResult.empty())
}
}
.observeOn(AndroidSchedulers.mainThread())
.doOnEach {
mainProgressbar.visibility = View.GONE
mainContent.visibility = View.VISIBLE
}
.doOnError { Snackbar.make(main_coordinator, "Error while searching", Snackbar.LENGTH_SHORT).show() }
.retry()
.subscribe({
mainContent.text = it.text
Log.d("MainActivity", it.text)
}, {
Log.e("MainActivity", it.toString())
})
Quite a journey, right? We avoided a couple of harmful bugs in our code and solved plenty of time-consuming mistakes. I hope it helped you.
Check out Kotlin Development.com for more interesting stories and code snippets written by real Kotlin devs from all around the globe! #hellokotlin
🌐 www.kotlindevelopment.com
🐤 Twitter: @hellokotlin
📧 hello@kotlindevelopment.com