Siguiendo con el apartado de búsquedas, verás que es posible desplegar una lista con sugerencias de consultas recientes en Android, a partir de los últimos términos que ha confirmado el usuario con el SearchView
.
Añadir esta característica permitirá evitarle al usuario escritura de texto, si es que desea realizar de nuevo alguna de sus búsquedas anteriores.
Ejemplo De Consultas Recientes En Android
En este tutorial aprenderás a crear una fuente de datos para guardar y limpiar sugerencias recientes en Android. Luego usarás el servicio de búsqueda del sistema para desplegar los resultados desde el SearchView
:
Es requisito que leas primero el tutorial sobre SearchView en Android, ya que usaremos la lista de gastos que construimos en él, junto a la implementación de su actividad de búsqueda.
Puedes descargar el proyecto en Android Studio desde el siguiente enlace. El módulo correspondiente se llama Sugerencias_Recientes
:
Almacenar Consultas Como Sugerencias
Antes de comenzar con los pasos de implementación, analicemos un poco el flujo de eventos sobre los componentes. El siguiente es un diagrama que muestra de forma simple el recorrido que haremos sobre los componentes de nuestra App de gastos:
Como ves, la secuencia de pasos se resume en:
- El usuario expande el SearchView
- El sistema consulta las últimas consultas y se muestran en una lista
- Flujos:
- Uso de sugerencias
- El usuario hace click en una sugerencia
- El sistema realiza la búsqueda con esa consulta
- El sistema muestra los resultados
- Nueva búsqueda
- El usuario escribe una nueva consulta y la confirma
- El sistema almacena esa consulta en la tabla de sugerencias
- El sistema muestra los resultados
- Uso de sugerencias
Analizando este algoritmo, sabemos que la mayoría de los eventos sobre la interfaz de búsqueda están controlados por el sistema.
Lo único que necesitamos añadir es una fuente de persistencia local que almacene a las consultas y las pueda limpiar si el usuario lo desea.
Con las anteriores responsabilidades y colaboraciones definidas, veamos cómo usar el framework para añadir sugerencias recientes a nuestra App de gastos.
1. Crear Persistencia Para Sugerencias
La primera herramienta de infraestructura que podría venirse a tu mente para guardar las consultas recientes podría ser la librería Room. La cual sería una buena opción si estuviésemos creando manualmente la interfaz de búsqueda.
Sin embargo, como estamos aprovechando la asistencia de los servicios de búsqueda de Android, usaremos una clase que nos facilita el trabajo: SearchRecentSuggestionsProvider
.
Este Content Provider envuelve toda la lógica para crear una tabla en SQLite con el fin de almacenar registros con los términos de búsqueda recientes del usuario.
Crear Content Provider De Sugerencias
En concreto, debes crear una implementación que extienda de SearchRecentSuggestionsProvider
. Para ello puedes hacer click derecho en tu paquete, selecciona New > Other >Content Provider y configura el nuevo provider:
O simplemente crea una clase llamada ExpenseSuggestionsProvider
como se ve en el siguiente código:
class ExpenseSuggestionsProvider : SearchRecentSuggestionsProvider() {
init {
setupSuggestions(AUTHORITY, MODE)
}
companion object {
const val AUTHORITY = "com.develou.ExpenseSuggestionsProvider"
const val MODE: Int = DATABASE_MODE_QUERIES
}
}
Y luego registralo en el AndroidManifest.xml
:
<application>
<provider
android:name=".ExpenseSuggestionsProvider"
android:authorities="com.develou.ExpenseSuggestionsProvider"
android:exported="false" />
<!--...-->
</application>
Es vital hacer la llamada de setupSuggestions()
en el bloque de inicialización para vincular el servicio de búsqueda con el esquema creado por el provider.
Los parámetros que reciben son la autoridad del provider y el modo. Por lo cual mantendremos sus valores en un objeto compañero.
Modo De La Base De Datos De Sugerencias
Dicho modo será DATABASE_MODE_QUERIES
para configurar la base de datos para que almacene las consultas recientes.
companion object {
const val AUTHORITY = "com.develou.ExpenseSuggestionsProvider"
const val MODE: Int = DATABASE_MODE_QUERIES or DATABASE_MODE_2LINES
}
Adicionalmente puedes realizar una operación de bits para incluir el modo DATABASE_MODE_2LINES
por si deseas añadir una segunda columna a la tabla interna de sugerencias, con el objetivo de sostener un texto de soporte para cada sugerencia.
2. Actualizar Archivo De Configuración De Búsqueda
El siguiente paso es decirle al servicio de búsqueda del sistema que nuestra búsqueda tendrá la característica sugerencias.
Esto se traduce en añadir nuevos atributos que soporten dicha configuración en tu archivo searchable.xml
:
<?xml version="1.0" encoding="utf-8"?>
<searchable xmlns:android="http://schemas.android.com/apk/res/android"
android:hint="@string/search_hint"
android:label="@string/app_name"
android:searchSuggestAuthority="com.develou.ExpenseSuggestionsProvider"
android:searchSuggestSelection=" ?" />
En particular usaremos dos atributos que describen esta funcionalidad: android:searchSuggestAuthority
y android:searchSuggestSelection
.
El primero le indica al sistema que la actividad de búsqueda mostrará sugerencias asociadas con el content provider al que le pertenece la autoridad asignada.
Y el segundo representa la sentencia en la cláusula WHERE
que use usa internamente al consultar sugerencias. Que como ves, usamos un espacio y el placeholder ?
para indicar que usaremos como parámetro la entrada del usuario.
3. Insertar Sugerencias
Aunque podríamos insertar consultas a través del ContentResolve
r y el método insert()
, el framework provee una clase auxiliar para cumplir esta tarea llamada SearchRecentSuggestions
.
Esta clase de utilidad envuelve toda la lógica del comando de inserción con su método saveRecentQuery()
.
Pero… ¿Cuál es el lugar adecuado para invocarlo?
Lo haces justo en el momento en que el servicio de búsqueda confirmó el String
enviado por el usuario, es decir, en el manejo del intent desde onCreate()
o onNewIntent()
en la actividad:
class MainActivity : AppCompatActivity() {
//...
private val suggestions = SearchRecentSuggestions(
this,
ExpenseSuggestionsProvider.AUTHORITY,
ExpenseSuggestionsProvider.MODE
)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
//...
}
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
handleSearchIntent(intent)
}
private fun handleSearchIntent(intent: Intent) {
if (Intent.ACTION_SEARCH == intent.action) {
intent.getStringExtra(SearchManager.QUERY)?.also { query ->
suggestions.saveRecentQuery(query, null)
searchExpenses(query)
}
}
}
private fun searchExpenses(query: String) {
val searchResults = ExpenseRepository.search(query)
(expenseList.adapter as ExpenseAdapter).submitList(searchResults)
}
//...
}
El constructor de SearchRecentSuggestions
recibe al contexto y a la autoridad y modo del content provider. Argumentos que ya tenemos a la mano.
Si ejecutas el aplicativo hasta el momento, ya podrás ver como se insertan las consultas confirmadas y como se muestra el dropdown con sugerencias infladas desde el provider:
4. Personalización De Ítem De Sugerencias
Debido a que estamos delegando la gran mayoría de eventos y colaboraciones al sistema de búsqueda de Android, el ítem de las sugerencias por defecto que nos provee no tiene el aspecto visual que requerimos:
Para que obtenga el aspecto que has vistos en las otras capturas, es necesario crear un layout que sea asignado desde al atributo searchViewStyle
en el tema asociado a la App Bar. Por ejemplo:
<style name="Theme.SearchViewEnAndroid.AppBarOverlay" parent="ThemeOverlay.AppCompat.Dark.ActionBar">
<item name="searchViewStyle">@style/SearchViewStyle</item>
</style>
Donde SearchViewStyle
hereda de Widget.AppCompat.SearchView.ActionBar
y usa como referencia al layout personalizado en el atributo suggestionRowLayout
:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="SearchViewStyle" parent="Widget.AppCompat.SearchView.ActionBar">
<item name="suggestionRowLayout">@layout/suggestion_item</item>
</style>
</resources>
¿Cómo construir el layout del ítem sin interferir con el código interno del adaptador de sugerencias?
Simplemente creas dos TextViews para el texto primario y el secundario que usen los recursos de ID @android:id/text1
y @android:id/text2
. Además de un elemento ImageView de legado con el id @android:id/query_image.
Aunque puedes agregar más elementos de decoración. Sin embargo, procesar los eventos de nuevos elementos ya requeriría que realices la búsqueda manualmente, desvinculando al SearchView
del servicio de búsqueda del sistema.
En particular, el layout anterior tendría la siguiente definición en el archivo res/layout/suggestion_item.xml
:
<?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:background="@color/white"
android:paddingHorizontal="16dp">
<ImageView
android:id="@+id/edit_query"
android:layout_width="0dp"
android:layout_height="0dp"
android:visibility="gone" />
<TextView
android:id="@android:id/text1"
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" />
<TextView
android:id="@android:id/text2"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:maxLines="1"
android:paddingHorizontal="32dp"
android:textColor="@color/black"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.975"
app:layout_constraintStart_toEndOf="@+id/suggestion_icon"
app:layout_constraintTop_toBottomOf="@android:id/text1" />
<ImageView
android:id="@+id/suggestion_icon"
android:layout_width="@dimen/icon_size"
android:layout_height="@dimen/icon_size"
android:contentDescription="@string/suggestion_icon"
android:src="@drawable/ic_suggestion"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
5. Limpiar Sugerencias
El altamente probable que el usuario desee eliminar el historial de sugerencias recientes que se le ofrecen para mantener su privacidad. Por lo cual debes proveer acceso a la acción de limpieza de las mismas:
La limpieza es similar al guardado, la clase SearchRecentSuggestion
s provee un método de utilidad llamado clearHistory()
, por lo que tu trabajo es tan solo decidir donde invocarlo.
Como ilustración, lo haremos en nuestra App de gastos desde el menú de opciones en la app bar:
override fun onOptionsItemSelected(item: MenuItem): Boolean {
if (item.itemId == R.id.clear_suggestions) {
suggestions.clearHistory()
return true
}
return super.onOptionsItemSelected(item)
}
Y como ya sabemos solo es manejar el evento de click desde el controlador onOptionsItemSelected()
del ítem que fue inflado desde el recurso de menú en onCreateOptiosnMenu()
:
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/search"
android:icon="@drawable/ic_search_black_24dp"
android:title="@string/search"
app:actionViewClass="androidx.appcompat.widget.SearchView"
app:showAsAction="collapseActionView|always" />
<item
android:id="@+id/clear_suggestions"
android:title="@string/clean_suggestions" />
</menu>
6. Manejar Eventos De Sugerencias
Probablemente surjan casos de uso donde requieras ejecutar tareas asociadas al click o selección de una sugerencia.
Por ejemplo, en ocasiones el framework ya no asigna la sugerencia seleccionada al campo de texto del SearchView
, por lo que nos toca asignarlo en tiempo de ejecución.
Un buen lugar para ello, es asignando una escucha OnSuggestionsListener
para intervenir el momento en que se hace click en una sugerencia:
searchView.setOnSuggestionListener(object : SearchView.OnSuggestionListener {
override fun onSuggestionSelect(position: Int): Boolean {
return false
}
override fun onSuggestionClick(position: Int): Boolean {
val suggestion = getClickedSuggestion(searchView, position)
searchView.setQuery(suggestion, false)
return false
}
})
Ambos controladores reciben el argumento de la posición en el adaptador de sugerencias que posee el AutoCompleteTextView
del SearchView.
En el momento en onSuggestionClick()
percibe que se hizo click en una sugerencia, aprovechamos para invocar a setQuery()
para reemplazar el texto en el SearchView
.
Pasamos false como segundo parámetro para no enviar la confirmación del cambio, ya que esta acción solo es cosmética.
Por otro lado, la obtención de la sugerencia a partir del adaptador lo logramos así:
private fun getClickedSuggestion(
searchView: SearchView,
position: Int
): String? {
val suggestionCursor = searchView.suggestionsAdapter.getItem(position) as Cursor
val suggestionIndex = suggestionCursor.getColumnIndex(SearchManager.SUGGEST_COLUMN_TEXT_1)
return suggestionCursor.getString(suggestionIndex)
}
Si revisas la implementación de la clase SuggestionsAdapter
asociado al SearchView
, verás que es del tipo CursorAdapter
. Por esta razón accedemos con la interfaz de Cursor para obtener el valor asociado a la columna SearchManager.SUGGEST_COLUMN_TEXT_1
.
Nota: Usa la propiedad SearchView.suggestionAdapter
para asignar tu propio adaptador para alimentar las sugerencias de búsqueda. Claramente tendrás que personalizar todo el ciclo para el procesamiento de la búsqueda, pero lograrás expandir sus comportamientos y estilos a lo que realmente necesitas.