Apuntes de cómo implementar un filtro de búsqueda en un recycleview, para eso se usará la implmentación Filterable combinando un ListAdapter.

ListAdapter con Filterable
class MyItemsListAdapter2(private val onItemClicked: (MyItemListUI) -> Unit) :
ListAdapter<MyItemListUI, MyItemsListAdapter2.MyViewHolder>(AsyncListDiffer()), Filterable {
companion object {
private const val TAG = "MyItemsListAdapter2"
}
private var completeItemList : MutableList<MyItemListUI>? = mutableListOf()
private class AsyncListDiffer : DiffUtil.ItemCallback<MyItemListUI>() {
override fun areItemsTheSame(oldItem: MyItemListUI, newItem: MyItemListUI) = oldItem.isSame(newItem)
override fun areContentsTheSame(oldItem: MyItemListUI, newItem: MyItemListUI) = oldItem.isContentSame(newItem)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyItemsListAdapter2.MyViewHolder {
val v = ItemSimpleRowBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return MyViewHolder(v)
}
override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
holder.bind(getItem(position))
}
inner class MyViewHolder(private val binding: ItemSimpleRowBinding) : RecyclerView.ViewHolder(binding.root) {
fun bind(item: MyItemListUI) {
val context = binding.root.context
binding.root.setOnClickListener { onItemClicked(item) }
binding.title.text = item.title
binding.category.text = item.category
}
}
fun setData(list: MutableList<MyItemListUI>?){
this.completeItemList = list
submitList(list)
}
override fun getFilter(): Filter {
//currentList
return customFilter
}
private val customFilter = object : Filter() {
override fun performFiltering(query: CharSequence?): FilterResults {
Log.d(TAG, "performFiltering: " + completeItemList?.count())
val filteredList = mutableListOf<MyItemListUI>()
if (query.isNullOrBlank()) {
completeItemList?.let { filteredList.addAll(it) }
} else {
for (item in completeItemList!!) {
if (item.title.contains(query, true)) {
filteredList.add(item)
}
}
}
val results = FilterResults()
results.values = filteredList
return results
}
@Suppress("UNCHECKED_CAST")
override fun publishResults(constraint: CharSequence?, filterResults: FilterResults) {
submitList(filterResults.values as MutableList<MyItemListUI>)
}
}
}
En customFilter es donde especificaremos el críterio de filtrado y para cargar los datos al adaptador usar setData
Añadir SearchView en la toolbar
Para añadir la vista SearchView en la toolbar se hace mediante un menú
<menu 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"
tools:context="com.webserveis.app.jsoneliners.com.MainActivity">
<item
android:id="@+id/action_search"
android:orderInCategory="100"
android:title="@string/action_search"
android:icon="@drawable/ic_search_24"
app:actionViewClass="androidx.appcompat.widget.SearchView"
app:showAsAction="always|collapseActionView" />
</menu>
Capturar el texto de SearView
Para poder capturar el texto que el usuario va poniendo en la vista SearchView en evento de carga de menús de la actividad o fragmento
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
inflater.inflate(R.menu.menu_list, menu)
val searchItem = menu.findItem(R.id.action_search)
val searchView = searchItem.actionView as SearchView
//searchView.setQuery("", false)
searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
override fun onQueryTextSubmit(query: String): Boolean {
mAdapter.filter.filter(query)
searchView.clearFocus()
return false
}
override fun onQueryTextChange(newText: String): Boolean {
//adapter.getFilter().filter(newText)
if (newText.isBlank()) mAdapter.filter.filter(newText)
return false
}
})
super.onCreateOptionsMenu(menu, inflater)
}
En la escucha de onQueryTextSubmit se obtiene el texto al enviar y en onQueryTextChange se detecta cualquier cambio que hace el usuario
Mostrar vista sin concidencias
Al caso que no haya concidencias con los críterios establecidos se debe mostrar una vista avisando al usuario que no hay concidencias, para eso se puede usar un observador del adaptador y detectar cuando está vacio para carga una vista o el RecyclerView
class EmptyDataObserver constructor(rv: RecyclerView?, ev: View?): RecyclerView.AdapterDataObserver() {
private var emptyView: View? = null
private var recyclerView: RecyclerView? = null
init {
recyclerView = rv
emptyView = ev
checkIfEmpty()
}
private fun checkIfEmpty() {
if (emptyView != null && recyclerView!!.adapter != null) {
val emptyViewVisible = recyclerView!!.adapter!!.itemCount == 0
emptyView!!.visibility = if (emptyViewVisible) View.VISIBLE else View.GONE
recyclerView!!.visibility = if (emptyViewVisible) View.GONE else View.VISIBLE
}
}
override fun onChanged() {
super.onChanged()
checkIfEmpty()
}
override fun onItemRangeChanged(positionStart: Int, itemCount: Int) {
super.onItemRangeChanged(positionStart, itemCount)
}
}
su uso
val emptyDataObserver = EmptyDataObserver(recycler_view, empty_view)
mAdapter.registerAdapterDataObserver(emptyDataObserver)
layout
<LinearLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior">
<com.webserveis.app.jsoneliners.views.EmptyRecyclerView
android:id="@+id/recycler_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scrollbars="vertical" />
<include
android:id="@+id/list_empty"
layout="@layout/view_search_empty" />
</LinearLayout>
<!-- .... view_search_empty -->
<?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"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="gone">
<ImageView
android:id="@+id/error_icon"
android:layout_width="96dp"
android:layout_height="96dp"
android:layout_marginStart="8dp"
android:layout_marginEnd="8dp"
android:layout_marginBottom="8dp"
android:alpha=".33"
android:contentDescription="@string/error_title"
app:layout_constraintBottom_toTopOf="@+id/error_title"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintVertical_chainStyle="packed"
app:srcCompat="@drawable/ic_search_24"
app:tint="?attr/colorControlNormal" />
<TextView
android:id="@+id/error_title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="16dp"
android:textAlignment="center"
android:textAppearance="@style/TextAppearance.MaterialComponents.Headline5"
android:textColor="?android:textColorPrimary"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
android:text="@string/error_search_empty_title" />
<TextView
android:id="@+id/error_summary"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:text="@string/error_search_empty_summary"
android:textAlignment="center"
android:textAppearance="@style/TextAppearance.MaterialComponents.Body1"
android:textColor="?android:textColorSecondary"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/error_title" />
</androidx.constraintlayout.widget.ConstraintLayout>
textos
<string name="error_search_empty_title">No matching result</string>
<string name="error_search_empty_summary">Try again using different search terms</string>