Alex
Alex

Reputation: 173

Filtering RecyclerView/ListAdapter with SearchView

I'm trying to implement a filter for my RecyclerView. I use data-binding and my adapter is a ListAdapter subclass as shown below

class BookAdapter(private val clickListener: ClickHandler) :
    ListAdapter<Book, BookAdapter.ViewHolder>(BooksDiffCallback()) {

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
        return ViewHolder.from(parent)
    }

    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
        holder.bind(getItem(position)!!, clickListener)
    }

    class ViewHolder private constructor(val binding: BookItemBinding) :
        RecyclerView.ViewHolder(binding.root) {

        fun bind(
            item: Book,
            clickListener: ClickHandler
        ) {
            binding.book = item
            binding.clickListener = clickListener
            binding.executePendingBindings()
        }

        companion object {
            fun from(parent: ViewGroup): ViewHolder {
                val inflater = LayoutInflater.from(parent.context)
                val binding = BookItemBinding.inflate(inflater, parent, false)
                return ViewHolder(binding)
            }
        }
    }
}

class BooksDiffCallback : DiffUtil.ItemCallback<Book>() {
    override fun areItemsTheSame(oldItem: Book, newItem: Book): Boolean {
        return oldItem.id == newItem.id
    }

    override fun areContentsTheSame(oldItem: Book, newItem: Book): Boolean {
        return oldItem == newItem
    }

}

class ClickHandler(val clickListener: (id: String) -> Unit) {
    fun onClick(item: Book) = clickListener(item.id)
}

According to the docs, to add filtering functionality I need to implement Filterable in my adapter and define getFilter() method. And this is where I stuck: I simply don't know how to implement getFilter() in the case of ListAdapter. Any help will be appreciated.

Upvotes: 14

Views: 7844

Answers (3)

Olayiwola Osho
Olayiwola Osho

Reputation: 189

Filter using ListAdapter

We will be using the filterable interface to help us filter (still figuring out why I shouldn't just use a filter function to get filteredLists and submitList(filteredLists) Directly)

Create your ListAdapter class

class BookAdapter(private val clickListener: ClickHandler) :
    ListAdapter<Book, BookAdapter.ViewHolder>(BooksDiffCallback()),Filterable

 {

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
    return ViewHolder.from(parent)
}

override fun onBindViewHolder(holder: ViewHolder, position: Int) {
    holder.bind(getItem(position)!!, clickListener)
}


override fun getFilter(): Filter {

return object : Filter() {

    override fun performFiltering(constraint: CharSequence?): FilterResults {

        return FilterResults().apply {

            values = if (constraint.isNullOrEmpty())
                mItems
            else
                onFilter(mItems, constraint.toString())
        }
    }

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

        submitList(results?.values as? List<Movies>)

    }
}

}

fun onFilter(list: List<Movies>, constraint: String) : List<Movies>{

val filteredList = list.filter {

    it.name.lowercase().contains(constraint.lowercase())

}

return filteredList

}


class ViewHolder private constructor(val binding: BookItemBinding) :
    RecyclerView.ViewHolder(binding.root) {

    fun bind(
        item: Book,
        clickListener: ClickHandler
    ) {
        binding.book = item
        binding.clickListener = clickListener
        binding.executePendingBindings()
    }

    companion object {
        fun from(parent: ViewGroup): ViewHolder {
            val inflater = LayoutInflater.from(parent.context)
            val binding = BookItemBinding.inflate(inflater, parent, false)
            return ViewHolder(binding)
        }
    }
}
}

class BooksDiffCallback : DiffUtil.ItemCallback<Book>() {
override fun areItemsTheSame(oldItem: Book, newItem: Book): Boolean {
    return oldItem.id == newItem.id
}

override fun areContentsTheSame(oldItem: Book, newItem: Book): Boolean {
    return oldItem == newItem
}

}

class ClickHandler(val clickListener: (id: String) -> Unit) {
    fun onClick(item: Book) = clickListener(item.id)
}

and in your MainActivity or Fragment Setup your adapter and your filter

private fun setupAdapter() {

adapter = BookAdapter(mItems)

recyclerView.adapter = adapter

}

fun filter(searchString : String){

adapter.filter.filter(searchString)

}

Upvotes: 1

ilatyphi95
ilatyphi95

Reputation: 605

I had a similar problem and tried solving it using a method similar to the one described by Maor Hadad above. it worked at some times and it raised cast error in the

Filter.publishResult()

method. So, I solved it this way. Firstly create a variable private var unfilteredlist = listOf<BaseDataItem>() and a methods

fun modifyList(list : List<BaseDataItem>) {
    unfilteredList = list
    submitList(list)
}

fun filter(query: CharSequence?) {
    val list = mutableListOf<BaseDataItem>()

    // perform the data filtering
    if(!query.isNullOrEmpty()) {
        list.addAll(unfilteredList.filter {
            it.*field1*.toLowerCase(Locale.getDefault()).contains(query.toString().toLowerCase(Locale.getDefault())) ||
                    it.*field2*.toLowerCase(Locale.getDefault()).contains(query.toString().toLowerCase(Locale.getDefault())) })
    } else {
        list.addAll(unfilteredList)
    }

    submitList(list)
}

in the BookAdapter class. Where *field1* and *field2*(you can add more fields) are the fields you want the search query to match. Then, wherever you call the adapter.submitList(List<BaseDataItem>) in the original code, replace it with the custom method adapter.modifyList(List<BaseDataItem>). Then write the searchView.setOnQueryTextListener like the one below

searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
    override fun onQueryTextSubmit(query: String): Boolean {

        return false
    }

    override fun onQueryTextChange(newText: String): Boolean {

        (binding.recycler.adapter as ItemAdapter).filter(newText)
        return true
    }
})

Don't forget to remove the Filterable interface and its methods, you don't need them anymore

Upvotes: 21

Maor Hadad
Maor Hadad

Reputation: 1872

I didn't find a way to obtain the list so I saved a reference to my list. Code example:

ListAdapter: Implement Filterable:

class ItemAdapter(private val clickListener: ItemListener) :
ListAdapter<ItemAdapter.BaseDataItem, RecyclerView.ViewHolder>(ItemDiffCallBack()), Filterable {

Add variables for reference:

var mListRef: List<BaseDataItem>? = null
var mFilteredList: List<BaseDataItem>? = null

Before you submit your list for the first time save it's referens

    withContext(Dispatchers.Main) {
        if (mListRef == null) {
                mListRef = items
            }
        submitList(items)
    }

The filter:

override fun getFilter(): Filter {

    return object : Filter() {
        override fun performFiltering(charSequence: CharSequence): FilterResults {

            val charString = charSequence.toString()

            if (charString.isEmpty()) {

                mFilteredList = mListRef
            } else {
                mListRef?.let {
                    val filteredList = arrayListOf<BaseDataItem>()
                    for (baseDataItem in mListRef!!) {
                        if (baseDataItem is BaseDataItem.DataItemWrapper) {
                            if (charString.toLowerCase(Locale.ENGLISH) in baseDataItem.dataItem.Name.toLowerCase(
                                    Locale.ENGLISH
                                )
                            ) {
                                filteredList.add(baseDataItem)
                            }
                        }
                    }

                    mFilteredList = filteredList
                }
            }
            val filterResults = FilterResults()
            filterResults.values = mFilteredList
            return filterResults
        }

        override fun publishResults(
            charSequence: CharSequence,
            filterResults: FilterResults
        ) {
            mFilteredList = filterResults.values as ArrayList<BaseDataItem>
            submitList(mFilteredList)
        }
    }
}

And if you search inside a fragment, add these:

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setHasOptionsMenu(true)
}

override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
    inflater.inflate(R.menu.menu_main, menu)
    val mSearchMenuItem = menu.findItem(R.id.search)
    val searchView = mSearchMenuItem.actionView as SearchView
    search(searchView)
    super.onCreateOptionsMenu(menu, inflater)
}


private fun search(searchView: SearchView) {

    searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
        override fun onQueryTextSubmit(query: String): Boolean {

            return false
        }

        override fun onQueryTextChange(newText: String): Boolean {

            (binding.recycler.adapter as ItemAdapter).filter.filter(newText)
            return true
        }
    })
}

Upvotes: 5

Related Questions