Cómo agregar un filtro de búsqueda a un Recyclerview, Filterable Android Kotlin


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>

Publicado por Webserveis

Desarrollador freelance programador apasionado por el arte de programar, amante del auto aprendizaje y interesado por la tecnología en general.

Deja una respuesta

Introduce tus datos o haz clic en un icono para iniciar sesión:

Logo de WordPress.com

Estás comentando usando tu cuenta de WordPress.com. Salir /  Cambiar )

Imagen de Twitter

Estás comentando usando tu cuenta de Twitter. Salir /  Cambiar )

Foto de Facebook

Estás comentando usando tu cuenta de Facebook. Salir /  Cambiar )

Conectando a %s

Este sitio usa Akismet para reducir el spam. Aprende cómo se procesan los datos de tus comentarios.

A %d blogueros les gusta esto: