Filterable RecyclerView in Android — The How To

Hamed Momeni
AndroidPub
Published in
5 min readAug 14, 2018

It happens quite often that we need a list of items in our Android application. And sometimes this list needs to have filtering capabilities for the users so they can find the relevant results in potentially a long list. In this post I am going to demonstrate how to achieve that purpose.

Move along, this is just for Medium’s sake

So first things first.

A look of what we’re trying to achieve

The ingredients…

We are going to use RxJava and RxBinding libraries for this example (I assume you are using them in your projects these days) and to sweeten things up we’ll be using the ViewModel package which is now a part of the Android JetPack toolbox.

The dependencies go as follows:

implementation 'io.reactivex.rxjava2:rxandroid:2.0.1'
implementation "io.reactivex.rxjava2:rxjava:2.1.9"
implementation "io.reactivex.rxjava2:rxkotlin:2.2.0"
implementation 'com.jakewharton.rxbinding2:rxbinding-kotlin:2.1.1'
implementation "android.arch.lifecycle:extensions:1.1.1"

Be sure to always use the latest versions of each library.

For the sake of this example I’ll be using the Post object to populate my RecyclerView with.

data class Post(
val id: Int,
val title: String,
val content: String
)

And a simple RecyclerView adapter.

class SimpleRecyclerAdapter(private val context: Context, private val posts: List<Post>) : RecyclerView.Adapter<SimpleRecyclerAdapter.PostHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PostHolder {
return PostHolder(LayoutInflater.from(context).inflate(R.layout.rcl_item_post, parent, false))
}

override fun getItemCount() = posts.size

override fun onBindViewHolder(holder: PostHolder, position: Int) {
holder.bind(posts[position])
}

class PostHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
fun bind(post: Post) {
itemView.titleView.text = post.title
itemView.contentView.text = post.content
}
}
}

Now we need a ViewModel for our MainActivity. Let’s name it MainViewModel .

class MainViewModel : ViewModel() {

private val originalPosts = listOf(
Post(1, "Ubi est castus animalis?", "Albus solems ducunt ad plasmator. Amors trabem in brigantium! Nutrix experimentums, tanquam camerarius ventus."),
Post(2, "Always spiritually gain the crystal power.", "When one traps conclusion and attitude, one is able to fear surrender.Yes, there is order, it empowers with bliss."),
Post(3, "Place the truffels in a casserole, and rub ultimately with whole teriyaki.", "Place the tuna in a pan, and mix carefully muddy with melted carrots lassi. Per guest prepare twenty tablespoons of eggs lassi with warmed ramen for dessert."),
Post(4, "Why does the scabbard fall?", "Jolly halitosis lead to the amnesty. The pants whines beauty like a rainy moon. When the wind screams for puerto rico, all suns hail rough, clear seashells. Scallywags sing from malarias like salty ships."),
Post(5, "Impressively translate a ferengi.", "The tragedy is an interstellar nanomachine. Wobble wihtout starlight travel, and we won’t promise a space. Meet wihtout nuclear flux, and we won’t transform a sonic shower. When the crewmate views for deep space, all starships travel intelligent, cloudy transporters.")
)

val filteredPosts: MutableList<Post> = mutableListOf()
val oldFilteredPosts: MutableList<Post> = mutableListOf()

init {
oldFilteredPosts.addAll(originalPosts)
}

}

So what are all those lists for?! Here is what they are for.

We are going to need 3 lists to hold our data in different states of the app.

  • originalPosts: This is the original list of posts that remains unchanged throughout the process of filtering the results. We always search against this list so it has to contain all the data in our scope. It usually comes form another source such as a database or a remote API.
  • oldFilteredPosts : This is the list that we pass to our ReyclerView adapter. It contains the final result of the filtering until the next search event.
  • filteredPosts : And this one’s a temporary list that we put our filtered data before applying it on the oldFiteredPosts . We store items here so that we can calculate the RecyclerView updates in order to have that nice animation you see up there.

So how do we make the RecyclerView to animate our searching and not just crudely reset the whole list. We use a tool called DiffUtil which is a part of the RecyclerView package itself.

DiffUtil has the responsibility of calculating the difference of each search iteration with the previous one and devising a series of action to convert the old list to the new one. It does so by using an implementation of DiffUtil.Callback interface to detect the individual item differences, which we will supply it with.

class PostsDiffUtilCallback(private val oldList: List<Post>, private val newList: List<Post>) : DiffUtil.Callback() {
override fun getOldListSize() = oldList.size

override fun getNewListSize() = newList.size

override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int) = oldList[oldItemPosition].id == newList[newItemPosition].id

override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int) = true // for the sake of simplicity we return true here but it can be changed to reflect a fine-grained control over which part of our views are updated
}

Now that we have our ingredients explained let’s see them in action. This part goes in our Activity:

viewModel = ViewModelProviders.of(this).get(MainViewModel::class.java)

recyclerView.layoutManager = LinearLayoutManager(this)
recyclerView.adapter = SimpleRecyclerAdapter(this, viewModel.oldFilteredPosts)

Before we can search in the list we need to add the search functionality to our ViewModel:

fun search(query: String): Completable = Completable.create {
val wanted = originalPosts.filter {
it
.title.contains(query) || it.content.contains(query)
}.toList()

filteredPosts.clear()
filteredPosts.addAll(wanted)
it.onComplete()
}

Explanation: Here we create a custom Completable object which notifies our RecyclerView of any changes in the list. Since we have direct access to the actual list using our ViewModel we won’t be using a Flowable or a Single object.

It is now time for the main part of our MainActivity which directs input text events to the ViewModel:

searchInput
.textChanges()
.debounce(500, TimeUnit.MILLISECONDS)
.subscribe {
viewModel
.search(it.toString())
.subscribeOn(Schedulers.computation())
.observeOn(AndroidSchedulers.mainThread())
.subscribe {
val diffResult = DiffUtil.calculateDiff(PostsDiffUtilCallback(viewModel.oldFilteredPosts, viewModel.filteredPosts))
viewModel.oldFilteredPosts.clear()
viewModel.oldFilteredPosts.addAll(viewModel.filteredPosts)
diffResult.dispatchUpdatesTo(recyclerView.adapter)
}
}

Explanation: We use .textChanges() method, courtesy of the RxBindings library, to get a stream of our EditText updates and then use .debounce() on it to only fire the search if the user has actually stopped typing to see the result. We don’t want to overflow our back-end with every single key stroke. You can adjust the time window. The lower the faster the updates.

Finally we send the search event to the ViewModel and tell it to run the search on a thread other than the UI Thread so we don’t freeze the UI if the searching process is heavy or from a network. Obviously we want the results main thread.

After calculating the diffResult we to the actual list updating here, clearing all the previous posts and adding the newly filtered ones:

viewModel.oldFilteredPosts.clear()
viewModel.oldFilteredPosts.addAll(viewModel.filteredPosts)

After that, it’s just the matter of updating the RecyclerView.

diffResult.dispatchUpdatesTo(recyclerView.adapter)

Using this technique you will have a nice animated RecyclerView filter capability. You can easily swap search function to read data from a database or a remote server.

That’s it.

You can access the full source code of this example at the following GitHub repository.

Feel free to leave a comment or give a clap 👏.

--

--

Hamed Momeni
AndroidPub

A programmer who’s fond of Android/Kotlin, Go, PHP and DevOps (Docker/K8s) among other things