How to add Search in RecyclerView using Kotlin

How to add Search in RecyclerView using Kotlin

Today, I will show you how to search for items in a RecyclerView using SearchView.

In this example, we have a RecyclerView already set up, and the only thing we do is to add the searching functionality.

We have a list of countries as sample data and using the search field we get the country we want faster.

Adding SearchView in RecyclerView

Add the SearchView in your layout, inside a LinearLayout with a vertical orientation.

<?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:id="@+id/country_content_id"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@color/colorPrimaryDark"
    android:orientation="vertical"
    tools:context=".MainActivity">

    <androidx.appcompat.widget.SearchView
        android:id="@+id/country_search"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="@color/colorPrimary"
        android:textCursorDrawable="@null"
        app:iconifiedByDefault="false"
        app:queryBackground="@null" />


    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/country_rv"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="@color/colorPrimaryDark" />

</LinearLayout>

In the RecyclerView adapter (In this example, RecyclerView_Adapter.kt), create an ArrayList with a name countryFilterList, and pass all the items..

class RecyclerView_Adapter(private var countryList: ArrayList<String>): RecyclerView.Adapter<RecyclerView.ViewHolder>() {

    var countryFilterList = ArrayList<String>()

    init {
        countryFilterList = countryList
    }
  
    // ...

}

Return the size of countryFilterList so every time will return the right amount of items that match the characters you typing in the SearchView.

class RecyclerView_Adapter(private var countryList: ArrayList<String>): RecyclerView.Adapter<RecyclerView.ViewHolder>() {

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

Now, in the onBindViewHolder get the item for each row from the countryFilterList array.

class RecyclerView_Adapter(private var countryList: ArrayList<String>): RecyclerView.Adapter<RecyclerView.ViewHolder>() {

    // ...

    override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
        // ...
        
        holder.itemView.select_country_text.text = countryFilterList[position]
        
        // ...
    }
}

Add the Filterable class in the RecyclerView_Adapter and the method getFilter()

class RecyclerView_Adapter(private var countryList: ArrayList<String>): RecyclerView.Adapter<RecyclerView.ViewHolder>(), Filterable {

    override fun getFilter(): Filter {
      
    }

}

Inside the getFilter() method, return a Filter() object:

class RecyclerView_Adapter(private var countryList: ArrayList<String>): RecyclerView.Adapter<RecyclerView.ViewHolder>(), Filterable {

    override fun getFilter(): Filter {
        return object : Filter() {
            override fun performFiltering(constraint: CharSequence?): FilterResults {
                
            }

            @Suppress("UNCHECKED_CAST")
            override fun publishResults(constraint: CharSequence?, results: FilterResults?) {
                
            }
          
        }
    }

}

The performFiltering method checks if we have typed a text in the SeachView.

If there is not any text, will return all items.

If there is a text, then we check if the characters match the items from the list and return the results in a FilterResults type.

The publishResults get these results, passes it to the countryFilterList array and updates the RecyclerView.

class RecyclerView_Adapter(private var countryList: ArrayList<String>): RecyclerView.Adapter<RecyclerView.ViewHolder>(), Filterable {

    override fun getFilter(): Filter {
        return object : Filter() {
            override fun performFiltering(constraint: CharSequence?): FilterResults {
                val charSearch = constraint.toString()
                if (charSearch.isEmpty()) {
                    countryFilterList = countryList
                } else {
                    val resultList = ArrayList<String>()
                    for (row in countryList) {
                        if (row.toLowerCase(Locale.ROOT).contains(charSearch.toLowerCase(Locale.ROOT))) {
                            resultList.add(row)
                        }
                    }
                    countryFilterList = resultList
                }
                val filterResults = FilterResults()
                filterResults.values = countryFilterList
                return filterResults
            }

            @Suppress("UNCHECKED_CAST")
            override fun publishResults(constraint: CharSequence?, results: FilterResults?) {
                countryFilterList = results?.values as ArrayList<String>
                notifyDataSetChanged()
            }
          
        }
    }

}

Go back to the Activity file (MainActivity.kt) and add the setOnQueryTextListener.

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        // ...

        country_search.setOnQueryTextListener(object: SearchView.OnQueryTextListener{
            override fun onQueryTextSubmit(query: String?): Boolean {
                return false
            }

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

        })
      
        // ...
      
    }
  
}

The onQueryTextChange method called every time we typing on the SearchView and updates the RecyclerView with the new results.

Customizing the SearchView

If you want to change the color of the search icon (magnifying glass):

class MainActivity : AppCompatActivity() {
  
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        // ...
      
        val searchIcon = country_search.findViewById<ImageView>(R.id.search_mag_icon)
        searchIcon.setColorFilter(Color.WHITE)

        // ...

    }

}

For the color of the cancel button:

class MainActivity : AppCompatActivity() {
  
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        // ...
      
        val cancelIcon = country_search.findViewById<ImageView>(R.id.search_close_btn)
        cancelIcon.setColorFilter(Color.WHITE)

        // ...

    }

}

And to change the text color of the TextView:

class MainActivity : AppCompatActivity() {
  
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        // ...
      
        val textView = country_search.findViewById<TextView>(R.id.search_src_text)
        textView.setTextColor(Color.WHITE)
        // If you want to change the color of the cursor, change the 'colorAccent' in colors.xml

        // ...

    }

}
You can find the final project here

If you have any questionsplease feel free to leave a comment below

Subscribe
Notify of
guest
43 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
akash

charSearch not defined?

akash

and also adapter.filter.filter?

Anuj

I can not get the adapter.filter.filter(newText) to work, would you be able to assist. Its highlighted in red and calls it an unsolved reference. I tried the workaround you posted in the comments and that also presents the same issue. The middle filter is a problem. I am using this in conjunction with an api connection to get the country names, but i do not see how it would present such an issue.

Mohammed

The (constraint) in the “performFiltering” method is not recognized ..

Mohammed

Yes, I did.
Sorry, the problem was in the parameter’s name which was “p0” not “constraint”.
Thank you.

Mohammed

Thank you very much

Lovanto

I got this problem lateinit property adapter has not been initialized How to fix it ?

Everton Rodrigues

HI, i cannot use toLowerCase() on row, do you have any idea why? toLowerCase() gives unresolved reference and “type mismatch”. I’m stuck on this part, please, help

Everton Rodrigues

I used toString before the toLowerCase() and it looks like it worked out. Thank you.

Everton Rodrigues

Looks like i can’t use resultList.add(row) if my class is Array(Class) and not a ArrayList(Class) 🙁

Everton Rodrigues

If i use ArrayList, List or MutableList, my Gson crashs telling me that it expected an object and got an array. Strangely, if i use an Array<MyClass>, it works and shows my data on the RecyclerView.

Dea Malaha

what if i use listOf<ToDo> ?

Dea Malaha

listOf<MyClass> . please explain to me i’m new to kotlin 🙂

Dea Malaha

No, not the .toLowerCase(), but on the resultList.add(row). Apparently i can’t use it too if i’m using listOf.

Dea Malaha

Yes, it is a data class. it kinda looks something like that. it’s a data class that carry Entity for my room database. I’m sorry if i can’t explain what i mean very well but I hope you understand. In my adapter class, i’m using listOf<ToDo>

Dea Malaha

Thank you so much. But one last question, what about the MainActivity? I mean i’m using room database, so it search for the things i want it to seach (for example the goal that you’ve just said). How should i write the function that do the filter? Because I’m seeing that you are not using room as i do so it must be kinda different than what you do in your MainActivity

Roshani

my item gets double

Roshani

no i am checking the list is correct. after that i tries with removeallviewinlayout this is working for one to two search then again some unrelated data get inflate.

Roshani

Let me try this

Joao Pedro

Where does the adapter in adapter.filter.filter(newText) comes from?
It’s highlighted in red and asks to create a variable adapter.

Codex
java.lang.NullPointerException: Attempt to invoke virtual method 'void androidx.appcompat.widget.SearchView.setOnQueryTextListener(androidx.appcompat.widget.SearchView$OnQueryTextListener)' on a null object reference

exception is being thrown for

setOnQueryTextListener(object : SearchView.OnQueryTextListener {

I have already tried this out and it worked. But now I am trying to replicate it but it’s throwing this exception. Any idea? If I comment this method, it doesn’t crash but then I won’t be able to use the search feature!

PS, I’m using this along with a PageAdapter, so I have 2 recycler view loading when I get into the fragment. Would that be a problem? Should I implement this for both the recycler views? I’ll give that a try in the meantime.

bhcvvvvj

It’s showing an error in “FilterResults()”

John

Hi,

I finish practice. Thank you. I have one question.

We listed all country and searchable. But I need when I click one of them open new page.

For example I am searching United Kingdom then I click and open introduction page united kingdom it was possible ?

Sampath Patro

I’m getting the following error for my code:

Type mismatch.
Required: SearchView.OnQueryTextListener!
Found:
 
Code:

searchBar.setOnQueryTextListener(object: SearchView.OnQueryTextListener{
    override fun onQueryTextSubmit(query: String?): Boolean {
        return false
    }

    override fun onQueryTextChange(query: String?): Boolean {
        adapter.filter.filter(query)
        return false
    }

})

The error is shown for “object: SearchView.OnQueryTextListener”