Searchable RecyclerView

Searchable RecyclerView

Saurabh Vashisht
Apr 11 · 3 min read

To make a searchable RecyclerView we need a RecyclerView (duh) and a SearchView which will accept the user query.

Lets design the layout first. The xml would look like this

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">

<androidx.appcompat.widget.SearchView
android:id="@+id/search_bar"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_margin="@dimen/dimen_12dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />


<androidx.recyclerview.widget.RecyclerView
android:id="@+id/city_name_list"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_margin="@dimen/dimen_12dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/search_bar"
app:layout_constraintVertical_bias="0"
tools:listitem="@layout/city_name_row_layout" />

</androidx.constraintlayout.widget.ConstraintLayout>

The xml layout of list item is :

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">

<TextView
android:id="@+id/city_code"
android:layout_width="@dimen/dimen_64dp"
android:layout_height="@dimen/dimen_64dp"
android:layout_margin="@dimen/dimen_12dp"
android:background="@drawable/circle_shape"
android:gravity="center"
android:padding="@dimen/dimen_12dp"
android:textColor="@color/grey_text"
android:textSize="@dimen/text_size_medium_large"
tools:text="DL" />

<TextView
android:id="@+id/city_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:textColor="@color/grey_text"
android:textSize="@dimen/text_size_medium"
tools:text="New Delhi" />
</LinearLayout>

This gives us a layout that looks like this

Screenshot of Searchable Recyclerview layout

I am assuming at this point that you have an adapter attached to your recycler view. Now to be able to filter the list as per a query entered by user, we need a filter. Add a filter to your RecyclerView’s Adapter

class CityListAdapter(private var cityDataList: ArrayList<CityDataObject>) :
RecyclerView.Adapter<CityViewHolder>() {



override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CityViewHolder {
//...
}

override fun onBindViewHolder(holder: CityViewHolder, position: Int) {
//...
}

override fun getItemCount(): Int {
return cityDataList.size
}

fun getFilter(): Filter {
return cityFilter
}

private val cityFilter = object : Filter() {
override fun performFiltering(constraint: CharSequence?): FilterResults {
val filteredList: ArrayList<CityDataObject> = ArrayList()
if (constraint == null || constraint.isEmpty()) {
cityDataList.let { filteredList.addAll(it) }
} else {
val query = constraint.toString().trim().toLowerCase()
cityDataList.forEach {
if (it.cityName.toLowerCase(Locale.ROOT).contains(query)) {
filteredList.add(it)
}
}
}
val results = FilterResults()
results.values = filteredList
return results
}

override fun publishResults(constraint: CharSequence?, results: FilterResults?) {
if (results?.values is ArrayList<*>) {
cityDataList.clear()
cityDataList.addAll(results.values as ArrayList<CityDataObject>)
notifyDataSetChanged()
}
}
}
}

Checkout this class at CityListAdapter’s implementation at github.

Now to accept the queries from user we attach a OnQueryTextListener to our SearchView which looks like this

searchBar.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
override fun onQueryTextSubmit(query: String?): Boolean {
adapter?.getFilter()?.filter(query)
return true
}

override fun onQueryTextChange(newText: String?): Boolean {
adapter?.getFilter()?.filter(newText);
return true
}

})

Voila. You now have a searchable RecyclerView.

Problems with above code: It works great when searching for the first time but not so much when you change the query. That is because in the filter implementation we made changes to the original list. And now it only contains search results of first query.

To solve this problem create a copy of the list and always perform search on this copy while publishing results on the original list or vice versa. I have highlighted the changes required below:

package com.example.recyclerview

import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Filter
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
import java.util.*
import kotlin.collections.ArrayList

class CityListAdapter(private var cityDataList: ArrayList<CityDataObject>) :
RecyclerView.Adapter<CityViewHolder>() {

// Create a copy of localityList that is not a clone
// (so that any changes in localityList aren't reflected in this //list)
val initialCityDataList = ArrayList<CityDataObject>().apply {
cityDataList?.let { addAll(it) }
}

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CityViewHolder {
//...
}

private val cityFilter = object : Filter() {
override fun performFiltering(constraint: CharSequence?): FilterResults {
val filteredList: ArrayList<CityDataObject> = ArrayList()
if (constraint == null || constraint.isEmpty()) {
initialCityDataList.let { filteredList.addAll(it) }
} else {
val query = constraint.toString().trim().toLowerCase()
initialCityDataList.forEach {
if (it.cityName.toLowerCase(Locale.ROOT).contains(query)) {
filteredList.add(it)
}
}
}
val results = FilterResults()
results.values = filteredList
return results
}

override fun publishResults(constraint: CharSequence?, results: FilterResults?) {
if (results?.values is ArrayList<*>) {
cityDataList.clear()
cityDataList.addAll(results.values as ArrayList<CityDataObject>)
notifyDataSetChanged()
}
}
}
}

After this your searchable recycler view should work everytime.

Demo gif of searchable recyclerview

Checkout the sample project for this article here:

If you find any issues with this article/code or have a suggestion leave a comment.

Geek Culture

Proud to geek out.

Sign up for Geek Culture Hits

By Geek Culture

Subscribe to receive top 10 most read stories of Geek Culture — delivered straight into your inbox, once a week. Take a look.

By signing up, you will create a Medium account if you don’t already have one. Review our Privacy Policy for more information about our privacy practices.

Check your inbox
Medium sent you an email at to complete your subscription.

Geek Culture

A new tech publication by Start it up (https://medium.com/swlh).

Saurabh Vashisht

Written by

Lazy programmer.

Geek Culture

A new tech publication by Start it up (https://medium.com/swlh).

Medium is an open platform where 170 million readers come to find insightful and dynamic thinking. Here, expert and undiscovered voices alike dive into the heart of any topic and bring new ideas to the surface. Learn more

Follow the writers, publications, and topics that matter to you, and you’ll see them on your homepage and in your inbox. Explore

If you have a story to tell, knowledge to share, or a perspective to offer — welcome home. It’s easy and free to post your thinking on any topic. Write on Medium

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store