Realizar Búsqueda Con SearchView Y Room

ANUNCIO
Loading...

Ahora pasaremos a crear una búsqueda con el SearchView y Room para dar un enfoque más realista a nuestra App de gastos. Tomaremos el control del comportamiento de la búsqueda a diferencia del tutorial anterior sobre sugerencias personalizadas.

Apariencia de ejemplo con SearchView y Room

En este tutorial, modificarás el proyecto de gastos actual para aprender lo siguiente:

  • Personalizar el comportamiento del SearchView
  • Buscar datos desde Room
  • Mostrar sugerencias de consultas recientes y personalizadas sin la asistencia de Android
  • Usar el ViewModel y LiveData en conjunto para soportar las actualizaciones de búsqueda

Nota: En este tutorial asumiré que ya leíste el apartado anterior Búsquedas Con Sugerencias Personalizadas y sus sucesores. Además de mis guías para Room, ViewModel y LiveData.


Ejemplo De Búsqueda Con SearchView Y Room

El aspecto visual de la App de gastos se mantiene igual para la lista principal, pero las sugerencias ahora serán parte del layout de la actividad principal. Esto se debe a que usaremos nuestra propia implementación en lugar del servicio de búsqueda de Android:

Descarga el proyecto final de la App desde el siguiente botón:

Abre el proyecto Android Studio y corre el módulo Busqueda_Room para probar la búsqueda y el despliegue de sugerencias. Esto te permitirá comprender mejor los pasos que presentaré a continuación.


Búsqueda Con Room

Hasta el momento habíamos usado a ExpenseRepository con una estrategia simplificada en memoria. Nuestros gastos vivían en una lista mutable del tipo Expense, que es nuestro modelo para gastos.

Para alcanzar la persistencia, reemplazaremos ese componente por un DAO para gastos y lo integraremos al manejo de estados de vista en un ViewModel y a la reactividad del LiveData.

Para cubrir este plan seguiremos los siguientes pasos:

  1. Crear base de datos Room de gastos
  2. Crear view model para actividad de gastos
  3. Actualizar lista de gastos al cambiar consulta de búsqueda
  4. Añadir sugerencias de consultas recientes
  5. Añadir sugerencias personalizadas

Con las tareas declaradas, pasemos a la acción.


1. Crear Base De Datos Room

Paso 1: Antes que nada añade las dependencias que usaremos en el proyecto al archivo build.gradle del proyecto:

plugins { id 'com.android.application' id 'kotlin-android' id 'kotlin-kapt' } android { //.. } dependencies { // Room implementation "androidx.room:room-runtime:2.3.0" implementation "androidx.room:room-ktx:2.3.0" kapt "androidx.room:room-compiler:2.3.0" // ViewModel implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.3.1") // LiveData implementation("androidx.lifecycle:lifecycle-livedata-ktx:2.3.1") // Extensiones KTX implementation "androidx.fragment:fragment-ktx:1.3.4" // RecyclerView implementation "androidx.recyclerview:recyclerview:1.2.1" // Default.. androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0' }
Lenguaje del código: JavaScript (javascript)

Paso 2: Luego crea la clase abstracta que extienda de RoomDatabase para que actúe como punto de entrada para SQLite. Su nombre será ExpensesDatabase:

@Database( entities = [Expense::class], version = 1 ) abstract class ExpensesDatabase : RoomDatabase() { abstract fun expenseDao(): ExpenseDao }
Lenguaje del código: CSS (css)

Paso 3: La entidad Expense de la anotación @Database marcará error en ejecución, ya que esta clase aún no está anotada con @Entity. En consecuencia, aplícale la marca:

@Entity(tableName = "expense") data class Expense( @PrimaryKey(autoGenerate = true) val id: Int, val description: String, val date: String, val total: Double )
Lenguaje del código: Kotlin (kotlin)

Como ves, por simplicidad del ejemplo, mapeamos directamente cada propiedad con la columna que dará soporte a la tabla. Donde id será la clave primaria con valores enteros autoincrementales.

Paso 4: Crea una nueva interfaz llamada ExpenseDao y añade los métodos insert(), query() y queryByDescription().

@Dao interface ExpenseDao { @Insert suspend fun insert(expense: Expense) @Insert fun insert(expense: List<Expense>) @Query("SELECT * FROM expense") suspend fun query(): List<Expense> @Query("SELECT * from expense WHERE description LIKE '%'|| :query ||'%'") suspend fun queryByDescription(query: String): List<Expense> }
Lenguaje del código: Kotlin (kotlin)

Evidentemente, insert() permitirá insertar uno o varios registros Expense, query() obtiene a todos los gastos y queryByDescription() obtiene los gastos que coincidan con el término de búsqueda en su columna expense.description.

Puntos a tener en cuenta:

  • Declaramos a los métodos como funciones suspendibles para ejecutarlos al interior de corrutinas y mantener el hilo de UI limpio.
  • Usamos el operador de concatenación de SQLite || para inyectar la consulta entre los placeholders de coincidencia de la cláusula LIKE.

Paso 5: Culminamos la creación de la base de datos, prepoblando con los diez gastos que tenemos en el repositorio de memoria:

//... abstract class ExpensesDatabase : RoomDatabase() { //... companion object { @Volatile private var INSTANCE: ExpensesDatabase? = null fun getInstance(context: Context): ExpensesDatabase = INSTANCE ?: synchronized(this) { INSTANCE ?: buildDatabase(context).also { db -> INSTANCE = db } } private fun buildDatabase(context: Context): ExpensesDatabase { return Room.databaseBuilder( context, ExpensesDatabase::class.java, "expenses.db" ).fallbackToDestructiveMigration() .addCallback(object : Callback() { override fun onCreate(db: SupportSQLiteDatabase) { // Prepoblar la base de datos con 10 gastos en hilo para IO ioThread { getInstance(context).expenseDao().insert(ExpenseRepository.getAll()) } } }) .build() } } } private val IO_EXECUTOR = Executors.newSingleThreadExecutor() fun ioThread(f: () -> Unit) { IO_EXECUTOR.execute(f) }
Lenguaje del código: Kotlin (kotlin)

Como ves, getInstance() es un método de provee la instancia de la base de datos. Y buildDatabase() usa el método Room.databaseBuilder() para configurar la creación.

Recuerda que Callback.onCreate() es llamado en el momento en que las tablas son creadas. Por esta razón es un buen lugar para prepoblar la tabla de gastos a través del ExpenseDao.insert() y nuestros datos del repositorio en memoria.


2. Crear ViewModel Para Gastos

Paso 1: Crea un nuevo ViewModel llamado ExpensesViewModel:

class ExpensesViewModel : ViewModel() { }
Lenguaje del código: Kotlin (kotlin)

Paso 2: Define como propiedades para los estado de vista de la consulta y la lista de gastos:

class ExpensesViewModel : ViewModel() { private val _textQuery = MutableLiveData<String>() val textQuery:LiveData<String> = _textQuery val expenses = MutableLiveData<List<Expense>>() init { initExpenses() } private fun initExpenses() { // Llamar a DAO } }
Lenguaje del código: Kotlin (kotlin)

Como ves, en el bloque init cargaremos a todos los gastos como estado inicial del live data expenses.

Paso 3: Define como propiedad del constructor primario a ExpenseDao con el objetivo de delegarle la obtención de gastos:

class ExpensesViewModel( private val expenseDao: ExpenseDao ) : ViewModel() { //.. private fun initExpenses() { viewModelScope.launch { expenses.value = expenseDao.query() } } } class ExpensesViewModelFactory( private val expenseDao: ExpenseDao ) : ViewModelProvider.Factory { override fun <T : ViewModel?> create(modelClass: Class<T>): T { @Suppress("UNCHECKED_CAST") if (modelClass.isAssignableFrom(ExpensesViewModel::class.java)) return ExpensesViewModel(expenseDao) as T throw IllegalArgumentException("Clase de ViewModel desconocida") } }
Lenguaje del código: Kotlin (kotlin)

Como el view model recibe parámetros, le creamos a la fábrica ExpensesViewModelFactory, con el fin de orientar al framework al crear el ejemplar de ExpensesViewModel.

Paso 4: Encuentra la referencia de ExpensesViewModel desde MainActivity aplicando delegación de propiedades con la función de extensión viewModels():

class MainActivity : AppCompatActivity() { private val viewModel by viewModels<ExpensesViewModel> { val db = ExpensesDatabase.getInstance(this) ExpensesViewModelFactory(db.expenseDao()) } }
Lenguaje del código: Kotlin (kotlin)

Luego registra un observador para expenses con setUpExpenses(). La razón para ello, es actualizar los datos del adaptador de expenseList a través de submitList():

private fun setUpList() { expenseList = findViewById(R.id.list) val adapter = ExpenseAdapter() expenseList.adapter = adapter viewModel.expenses.observe(this){ listedExpenses -> adapter.submitList(listedExpenses) } }
Lenguaje del código: Kotlin (kotlin)

3.Usar Toolbar Como View Autónomo

Aunque seguiremos usando a la clase SearchView, esta vez necesitamos añadir una App Bar para conseguir su referencia como un view normal.

App Bar para app de gastos

Modificar Layout De Actividad

El proceso ya lo sabemos. Abrimos activity_main.xml y declaramos el código estándar de una pantalla con app bar y contenido principal:

<?xml version="1.0" encoding="utf-8"?> <androidx.coordinatorlayout.widget.CoordinatorLayout 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:layout_width="match_parent" android:layout_height="match_parent" tools:context=".ui.view.MainActivity"> <com.google.android.material.appbar.AppBarLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:theme="@style/Theme.SearchViewEnAndroid.AppBarOverlay"> <androidx.appcompat.widget.Toolbar android:id="@+id/toolbar" android:layout_width="match_parent" android:layout_height="?attr/actionBarSize" android:background="?attr/colorPrimary" app:popupTheme="@style/Theme.SearchViewEnAndroid.PopupOverlay" /> </com.google.android.material.appbar.AppBarLayout> <include layout="@layout/content_main" /> </androidx.coordinatorlayout.widget.CoordinatorLayout>
Lenguaje del código: HTML, XML (xml)

Configurar Toolbar

Luego desde onCreate(), llamamos un método de utilidad que tome la referencia de la Toolbar, cambie su título, infle el menú de gastos que tenemos y configure el SearchView:

private fun setUpToolbar() { toolbar = findViewById(R.id.toolbar) toolbar.run { setTitle(R.string.app_name) inflateMenu(R.menu.expenses_menu) setUpSearchView(menu) } }
Lenguaje del código: Kotlin (kotlin)

Configurar SearchView

Al interior del método expresado setUpSearchView() haremos todo lo que hacíamos antes en onCreateOptionsMenu():

  1. Obtener referencia de SearchView
  2. Registrar escucha de expansión y contracción
  3. Registrar escucha para limpieza de consulta
  4. Registrar escucha de cambio del texto de consulta

Es decir:

private fun setUpSearchView(menu: Menu) { val searchItem = menu.findItem(R.id.search) searchItem.setOnActionExpandListener(object : MenuItem.OnActionExpandListener { override fun onMenuItemActionExpand(item: MenuItem?): Boolean { return true } override fun onMenuItemActionCollapse(item: MenuItem?): Boolean { return true } }) searchView = searchItem.actionView as SearchView searchView.run { queryHint = getString(R.string.search_hint) findViewById<View>(R.id.search_close_btn).setOnClickListener { setQuery("", true) showKeyboard(this) } setOnQueryTextListener(object : SearchView.OnQueryTextListener { override fun onQueryTextSubmit(query: String): Boolean { dismissKeyboard(searchView) return true } override fun onQueryTextChange(newText: String): Boolean { return true } }) } }
Lenguaje del código: Kotlin (kotlin)

Ya que esta vez el sistema no nos ayudará con los comportamientos para mostrar/ocultar el teclado, usaremos los siguientes métodos de utilidad desde la limpieza y el envío de consulta:

private fun showKeyboard(view: View) { ViewCompat.getWindowInsetsController(view)?.show(WindowInsetsCompat.Type.ime()) } private fun dismissKeyboard(view: View) { ViewCompat.getWindowInsetsController(view)?.hide(WindowInsetsCompat.Type.ime()) }
Lenguaje del código: Kotlin (kotlin)

4. Manejar Envío De Búsqueda

En este momento el SearchView se expande y recibe resultados de tus consultas. No obstante, no tendrás ningún comportamiento por obvias razones.

Primero tenemos que actualizar la consulta que tenemos en el view model desde onQueryTextChange():

override fun onQueryTextChange(newText: String): Boolean { viewModel.onSearchQueryChanged(newText) return true }
Lenguaje del código: Kotlin (kotlin)

El método onSeaerchQueryChanged() responderá asignando el nuevo valor:

fun onSearchQueryChanged(query: String) { _textQuery.value = query }
Lenguaje del código: Kotlin (kotlin)

Luego, ejecuta la búsqueda al confirmar la consulta, ve al controlador onQueryTextSubmit() y haz el llamado del View Model:

override fun onQueryTextSubmit(query: String): Boolean { viewModel.onSearchQuerySubmitted() dismissKeyboard(searchView) return true }
Lenguaje del código: Kotlin (kotlin)

El método onSearchQuerySubmitted() transmite el comando de cambio de consulta hacia los estados de vista, de forma que busquemos desde la base de datos los gastos asociados.

fun onSearchQuerySubmitted() { val query = textQuery.value if (query.isNullOrBlank()) return viewModelScope.launch { expenses.value = expenseDao.queryByDescription(query) } }
Lenguaje del código: Kotlin (kotlin)

5. Manejar Colapso Del SearchView

En el momento que el SearchView se contrae, debemos retornar a la lista original de todos los gastos. Para desembocar este estímulo, llamamos a su contraparte del view model:

override fun onMenuItemActionCollapse(item: MenuItem?): Boolean { viewModel.onSearchClosed() return true }
Lenguaje del código: Kotlin (kotlin)

Con el método onSearchClosed() limpiaremos los resultados de búsqueda:

fun onSearchClosed() { initExpenses() }
Lenguaje del código: Kotlin (kotlin)

Con el código anterior, ya se disparará la actualización de la lista de gastos desde Room, cada que el usuario presiona el botón de enviar búsqueda:

Sin embargo, en el momento en que se expande el SearchView deseamos presentar sugerencias, es decir, reemplazar la lista de gastos por una lista de sugerencias mientras la búsqueda esté activa.

Veamos cómo hacerlo.


6. Añadir Sugerencias Recientes Desde Room

Si recuerdas, en el tutorial sobre sugerencias recientes, vimos que existía una tabla adicional para operarlas y consultarlas.

Búsqueda con sugerencias recientes y Room

Pues en esta solución haremos exactamente lo mismo, solo que sin el ContentProvider y aplicando Room. Observa.

Crear Tabla De Consultas Recientes

Iniciemos añadiendo una nueva entidad llamada RecentQuery que contenga como columnas un identificador, el término de búsqueda y la fecha de inserción.

@Entity(tableName = "recent_query", indices = [Index(value = ["text"], unique = true)]) data class RecentQuery( @PrimaryKey(autoGenerate = true) val id: Int = 0, val text: String, val createdDate: Long = System.currentTimeMillis() )
Lenguaje del código: Kotlin (kotlin)

Importantisimo agregar esta nueva entidad a la base de datos:

@Database( entities = [Expense::class, RecentQuery::class], version = 1 )
Lenguaje del código: Kotlin (kotlin)

Crear DAO De Consultas Recientes

Seguido, añade su respectivo objeto de acceso con el nombre de RecentQueryDao. Provee métodos para inserción, consulta y borrado:

@Dao interface RecentQueryDao { @Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun insert(query: RecentQuery) @Query("SELECT * FROM recent_query") fun query(): LiveData<List<RecentQuery>> @Query("SELECT * FROM recent_query WHERE text LIKE '%'|| :query ||'%'") fun queryByText(query: String): LiveData<List<RecentQuery>> @Query("DELETE FROM recent_query") suspend fun delete() }
Lenguaje del código: Kotlin (kotlin)

Y claro está, añade su acceso desde la base de datos:

abstract class ExpensesDatabase : RoomDatabase() { //... abstract fun recentQueryDao(): RecentQueryDao }
Lenguaje del código: Kotlin (kotlin)

Terminando este fragmento de infraestructura, ahora avancemos a la UI.

Añadir LiveData Para Sugerencias Recientes

Para nuestro ejemplo de gastos añadiremos un segundo RecyclerView, con el objetivo de jugar con las visibilidades de los gastos y las sugerencias recientes.

Esto quiere decir que añadiremos un nuevo estado de vista para las sugerencias en ExpensesViewModel:

val recentSuggestions: LiveData<List<RecentQuery>> = textQuery.switchMap { query -> if (query.isBlank()) recentQueryDao.query() else recentQueryDao.queryByText(query) }.distinctUntilChanged()
Lenguaje del código: Kotlin (kotlin)

La lista de sugerencias reacciona en cadena con la consulta de texto, por lo que usamos la función switchMap() para realizar la vinculación hacia las consultas de sugerencias del DAO.

Si la consulta está en blanco, tomamos todas las sugerencias (o el número que desees), de lo contrario las filtramos por el texto.

Nota: Recuerda que distinctUntilChanged() evita que el LiveData emita valores hasta que sean diferentes.

Crear Adaptador De Sugerencias Recientes

El siguiente paso es crear el adaptador que mostrará y procesará los clicks sobre cada sugerencia.

Por lo que crea la clase RecentSuggestionsAdapter y configura el siguiente esquema:

class RecentSuggestionAdapter(private val listener: (RecentQuery) -> Unit) : ListAdapter<RecentQuery, RecentSuggestionAdapter.SuggestionViewHolder>( SuggestionDiffCallback() ) { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SuggestionViewHolder { val view = LayoutInflater .from(parent.context) .inflate(R.layout.recent_suggestion_item, parent, false) return SuggestionViewHolder(view, listener) } override fun onBindViewHolder(holder: SuggestionViewHolder, position: Int) { val suggestion = getItem(position) holder.bind(suggestion) } class SuggestionViewHolder(view: View, val listener: (RecentQuery) -> Unit) : RecyclerView.ViewHolder(view) { private lateinit var currentItem: RecentQuery private val suggestionText = view.findViewById<TextView>(R.id.suggestion) init { itemView.setOnClickListener { listener(currentItem) } } fun bind(suggestion: RecentQuery) { currentItem = suggestion suggestionText.text = suggestion.text } } } class SuggestionDiffCallback : DiffUtil.ItemCallback<RecentQuery>() { override fun areItemsTheSame(oldItem: RecentQuery, newItem: RecentQuery): Boolean { return oldItem.id == newItem.id } override fun areContentsTheSame(oldItem: RecentQuery, newItem: RecentQuery): Boolean { return oldItem == newItem } }
Lenguaje del código: Kotlin (kotlin)

El layout usado para el ítem es prácticamente la copia del que usamos en el tutorial de sugerencias recientes:

Layout de sugerencia reciente para búsqueda con Room

Diseñamos un espacio con un icono de aspecto temporal y el texto de la consulta:

<?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" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="56dp" android:foreground="?attr/selectableItemBackground" android:paddingHorizontal="16dp"> <TextView android:id="@+id/suggestion" android:layout_width="0dp" android:layout_height="wrap_content" android:maxLines="1" android:paddingHorizontal="32dp" android:textAppearance="@style/TextAppearance.MaterialComponents.Body1" android:textColor="@color/black" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toEndOf="@+id/suggestion_icon" app:layout_constraintTop_toTopOf="parent" tools:text="Sugerencia" /> <ImageView android:id="@+id/suggestion_icon" android:layout_width="24dp" android:layout_height="24dp" android:src="@drawable/ic_recent_suggestion" android:contentDescription="@string/recent_suggestion_icon" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> </androidx.constraintlayout.widget.ConstraintLayout>
Lenguaje del código: HTML, XML (xml)

Observar Lista De Sugerencias Recientes

Al igual que con los gastos, toma la referencia del RecyclerView asociado a las sugerencias, asigna su adaptador y termina usando observe() sobre recentSuggestions para actualizar sus datos internos en cada cambio:

private fun setUpSuggestions() { suggestionsList = findViewById(R.id.suggestions_list) val recentAdapter = RecentSuggestionAdapter { suggestion -> searchView.setQuery(suggestion.text, false) viewModel.onRecentQueryClicked(suggestion) dismissKeyboard(searchView) } suggestionsList.adapter = recentAdapter viewModel.recentSuggestions.observe(this) { listedSuggestions -> recentAdapter.submitList(listedSuggestions) } }
Lenguaje del código: Kotlin (kotlin)

En el código anterior destacan las acciones que ejecutamos en el click de las sugerencias recientes:

  1. Se actualiza la consulta del SearchView con el texto de la sugerencia clickeada
  2. Se procesa el click desde el view model
  3. Se oculta el teclado

El método onRecentQueryClicked() se compone del guardado de la consulta y la confirmación de una búsqueda:

fun onRecentQueryClicked(recentQuery: RecentQuery) { _textQuery.value = recentQuery.text onSearchQuerySubmitted() }
Lenguaje del código: Kotlin (kotlin)

Insertar Consultas Recientes

Lo que falta ahora es insertar en la tabla las consultas recientes del usuario, lo cual es sencillo. Desde onSearchQuerySubmitted() invoca a saveRecentQuery() para insertar el registro:

fun onSearchQuerySubmitted() { val query = textQuery.value if (query.isNullOrBlank()) return saveRecentQuery(query) viewModelScope.launch { expenses.value = expenseDao.queryByDescription(query) } } private fun saveRecentQuery(query: String) { viewModelScope.launch { val recentQuery = RecentQuery(text = query) recentQueryDao.insert(recentQuery) } }
Lenguaje del código: Kotlin (kotlin)

De esta forma, cada que presiones el icono de envío, tendrás una nueva consulta reciente en la tabla. Puedes usar al Database Inspector para comprobar las operaciones:

Database Inspector con Room y sugerencias

7. Añadir Sugerencias Personalizadas Desde Room

Y por último, combinaremos las sugerencias recientes y las personalizadas en una misma lista proyectada en la pantalla de gastos:

Búsqueda con sugerencias personalizadas y Room

Procesar el estado de vista para estas sugerencias es casi identico al caso anterior, por lo que no ahondaremos mucho en explicaciones sobre la creación de los componentes.

Origen De Las Sugerencias Personalizadas

La fuente de datos desde donde se crean las sugerencias personalizadas es la misma tabla expense. No obstante, solo necesitaremos el id y la descripción, por lo que crearemos la siguiente vista en la base de datos:

@DatabaseView( viewName = "expense_suggestion_view", value = "SELECT id as expenseId, description as text FROM expense" ) data class ExpenseSuggestionView( val expenseId: Int, val text: String )
Lenguaje del código: Kotlin (kotlin)

Al igual que las entidades, debes añadir la vista a la base de datos:

@Database( entities = [Expense::class, RecentQuery::class], views = [ExpenseSuggestionView::class], version = 1 )
Lenguaje del código: Kotlin (kotlin)

Y completamos con un método de consulta desde ExpensesDao:

@Query("SELECT * FROM expense_suggestion_view WHERE text LIKE '%'|| :query ||'%'") fun suggestions(query: String): LiveData<List<ExpenseSuggestionView>>
Lenguaje del código: Kotlin (kotlin)

Añadir LiveData De Sugerencias Personalizadas

El estado de vista es exactamente que el de las sugerencias recientes. Responde al cambio del live data asociado a la query de sugerencias:

val customSuggestions: LiveData<List<ExpenseSuggestionView>> = textQuery.switchMap { query -> if (query.length > 1) expenseDao.suggestions(query) else MutableLiveData(emptyList()) }.distinctUntilChanged()
Lenguaje del código: Kotlin (kotlin)

La diferencia radica en que solo comenzamos a buscar si el tamaño del texto de consulta es mayor a 1. De lo contrario retornamos un live data vacío.

Crear Adaptador Para Sugerencias Personalizadas

De igual forma creamos un adaptador para inflar el layout para sugerencias personalizadas y bindee a los objetos ExpenseSuggestionView:

class CustomSuggestionAdapter(private val listener: (ExpenseSuggestionView) -> Unit) : ListAdapter<ExpenseSuggestionView, CustomSuggestionAdapter.SuggestionViewHolder>( CustomDiffCallback() ) { class SuggestionViewHolder(view: View, val listener: (ExpenseSuggestionView) -> Unit) : RecyclerView.ViewHolder(view) { private lateinit var currentItem: ExpenseSuggestionView private val suggestionText = view.findViewById<TextView>(R.id.suggestion) init { itemView.setOnClickListener { listener(currentItem) } } fun bind(suggestion: ExpenseSuggestionView) { currentItem = suggestion suggestionText.text = suggestion.text } } override fun onCreateViewHolder( parent: ViewGroup, viewType: Int ): SuggestionViewHolder { val view = LayoutInflater .from(parent.context) .inflate(R.layout.custom_suggestion_item, parent, false) return SuggestionViewHolder(view, listener) } override fun onBindViewHolder( holder: SuggestionViewHolder, position: Int ) { val suggestion = getItem(position) holder.bind(suggestion) } } class CustomDiffCallback : DiffUtil.ItemCallback<ExpenseSuggestionView>() { override fun areItemsTheSame( oldItem: ExpenseSuggestionView, newItem: ExpenseSuggestionView ): Boolean { return oldItem.expenseId == newItem.expenseId } override fun areContentsTheSame( oldItem: ExpenseSuggestionView, newItem: ExpenseSuggestionView ): Boolean { return oldItem == newItem } }
Lenguaje del código: Kotlin (kotlin)

El layout es la réplica de las recientes, solo que el icono cambia por una lupa:

Layout de sugerencia personalizada

Mantendremos esta separación de layouts para abrir los diseños a futuras personalizaciones y eventos:

<?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" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="56dp" android:foreground="?attr/selectableItemBackground" android:paddingHorizontal="16dp"> <TextView android:id="@+id/suggestion" android:layout_width="0dp" android:layout_height="wrap_content" android:maxLines="1" android:paddingHorizontal="32dp" android:textAppearance="@style/TextAppearance.MaterialComponents.Body1" android:textColor="@color/black" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toEndOf="@+id/suggestion_icon" app:layout_constraintTop_toTopOf="parent" tools:text="Sugerencia" /> <ImageView android:id="@+id/suggestion_icon" android:layout_width="24dp" android:layout_height="24dp" android:contentDescription="@string/custom_suggestion_icon" android:src="@drawable/ic_search_black_24dp" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> </androidx.constraintlayout.widget.ConstraintLayout>
Lenguaje del código: HTML, XML (xml)

Fusionar Ambos Tipos De Sugerencias Con ConcatAdapter

La librería del RecyclerView nos provee la clase ConcatAdapter para integrar dos o más adaptadores sobre una lista.

Y esto nos viene perfecto, ya que deseamos solucionar la combinación de los resultados desde la UI para mantener los eventos de cada ítem separados:

private fun setUpSuggestions() { suggestionsList = findViewById(R.id.suggestions_list) val recentAdapter = RecentSuggestionAdapter { suggestion -> searchView.setQuery(suggestion.text, false) viewModel.onRecentQueryClicked(suggestion) dismissKeyboard(searchView) } val customAdapter = CustomSuggestionAdapter { suggestion -> goToExpenseDetail(suggestion.expenseId) } suggestionsList.adapter = ConcatAdapter(recentAdapter, customAdapter) viewModel.recentSuggestions.observe(this) { listedSuggestions -> recentAdapter.submitList(listedSuggestions) } viewModel.customSuggestions.observe(this) { customAdapter.submitList(it) } }
Lenguaje del código: Kotlin (kotlin)

En el momento en que cambien las listas de ambas sugerencias, los observadores sobre los LiveDatas invocarán la actualización sobre la instancia correspondiente. Aportándonos así, una lista con dos diferentes tipos de sugerencias:

Sugerencias recientes y personalizadas en búsqueda con Room

Más Contenidos Android

¿Ha sido útil esta publicación?