Reputation: 173
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
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
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
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