Implementando búsqueda instantánea en Android usando RxJava

Implementando búsqueda instantánea en Android usando RxJava


Estoy trabajando en una nueva aplicación que, como suele suceder, se comunica con el servicio de back-end para recibir datos a través de la API. En este ejemplo, desarrollaré una función de búsqueda, una de cuyas características será una búsqueda instantánea al ingresar texto.


Búsqueda instantánea


Nada complicado, piensas. Solo necesita colocar el componente de búsqueda en la página (muy probablemente, en la barra de herramientas), conectar el onTextChange eventos onTextChange y realizar la búsqueda. Entonces, esto es lo que hice:


 override fun onCreateOptionsMenu(menu: Menu?): Boolean { menuInflater.inflate(R.menu.menu_main, menu) val searchView = menu?.findItem(R.id.action_search)?.actionView as SearchView // Set up the query listener that executes the search searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener { override fun onQueryTextSubmit(query: String?): Boolean { Log.d(TAG, "onQueryTextSubmit: $query") return false } override fun onQueryTextChange(newText: String?): Boolean { Log.d(TAG, "onQueryTextChange: $newText") return false } }) return super.onCreateOptionsMenu(menu) } 

Pero aquí está el problema. Como necesito implementar una búsqueda correcta durante la entrada, cada vez que se onQueryTextChange() un controlador de eventos onQueryTextChange() a la API para obtener el primer conjunto de resultados. Los registros son los siguientes:


 D/MainActivity: onQueryTextChange: T D/MainActivity: onQueryTextChange: TE D/MainActivity: onQueryTextChange: TES D/MainActivity: onQueryTextChange: TEST D/MainActivity: onQueryTextSubmit: TEST 

A pesar de que solo estoy imprimiendo mi solicitud, hay cinco llamadas API, cada una de las cuales realiza una búsqueda. Por ejemplo, en la nube, debe pagar por cada llamada a la API. Por lo tanto, cuando ingreso mi solicitud, necesito un pequeño retraso antes de enviarla, para que al final solo se realice una llamada a la API.


Ahora, supongamos que quiero encontrar algo más. Elimino TEST e ingreso otros caracteres:


 D/MainActivity: onQueryTextChange: TES D/MainActivity: onQueryTextChange: TE D/MainActivity: onQueryTextChange: T D/MainActivity: onQueryTextChange: D/MainActivity: onQueryTextChange: S D/MainActivity: onQueryTextChange: SO D/MainActivity: onQueryTextChange: SOM D/MainActivity: onQueryTextChange: SOME D/MainActivity: onQueryTextChange: SOMET D/MainActivity: onQueryTextChange: SOMETH D/MainActivity: onQueryTextChange: SOMETHI D/MainActivity: onQueryTextChange: SOMETHIN D/MainActivity: onQueryTextChange: SOMETHING D/MainActivity: onQueryTextChange: SOMETHING D/MainActivity: onQueryTextChange: SOMETHING E D/MainActivity: onQueryTextChange: SOMETHING EL D/MainActivity: onQueryTextChange: SOMETHING ELS D/MainActivity: onQueryTextChange: SOMETHING ELSE D/MainActivity: onQueryTextChange: SOMETHING ELSE D/MainActivity: onQueryTextSubmit: SOMETHING ELSE 

¡Hay 20 llamadas API! Un pequeño retraso reducirá la cantidad de estas llamadas. También quiero deshacerme de los duplicados para que el texto recortado no conduzca a solicitudes repetidas. Probablemente también quiera filtrar algunos elementos. Por ejemplo, ¿necesita la capacidad de buscar sin los caracteres ingresados ​​o buscar por un carácter?


Programación reactiva


Hay varias opciones sobre qué hacer a continuación, pero en este momento quiero centrarme en una técnica que se conoce comúnmente como programación reactiva y la biblioteca RxJava. Cuando me encontré con la programación reactiva, vi la siguiente descripción:


ReactiveX es una API que funciona con estructuras asincrónicas y manipula flujos de datos o eventos utilizando combinaciones de patrones de observador e iterador, así como funciones de programación funcional.

Esta definición no explica completamente la naturaleza y las fortalezas de ReactiveX. Y si explica, solo a aquellos que ya están familiarizados con los principios de este marco. También vi gráficos como este:


Tabla de Operador de Retraso


El diagrama explica el papel del operador, pero no comprende completamente la esencia. Entonces, veamos si puedo explicar más claramente este diagrama con un ejemplo simple.


Preparemos nuestro proyecto primero. Necesitará una nueva biblioteca en el archivo build.gradle de su aplicación:


 implementation "io.reactivex.rxjava2:rxjava:2.1.14" 

Recuerde sincronizar las dependencias del proyecto para cargar la biblioteca.


Ahora veamos una nueva solución. Usando el método anterior, accedí a la API cuando ingresé cada nuevo personaje. Usando el nuevo método, voy a crear un Observable :


 override fun onCreateOptionsMenu(menu: Menu?): Boolean { menuInflater.inflate(R.menu.menu_main, menu) val searchView = menu?.findItem(R.id.action_search)?.actionView as SearchView // Set up the query listener that executes the search Observable.create(ObservableOnSubscribe<String> { subscriber -> searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener { override fun onQueryTextChange(newText: String?): Boolean { subscriber.onNext(newText!!) return false } override fun onQueryTextSubmit(query: String?): Boolean { subscriber.onNext(query!!) return false } }) }) .subscribe { text -> Log.d(TAG, "subscriber: $text") } return super.onCreateOptionsMenu(menu) } 

Este código hace exactamente lo mismo que el código anterior. Los registros son los siguientes:


 D/MainActivity: subscriber: T D/MainActivity: subscriber: TE D/MainActivity: subscriber: TES D/MainActivity: subscriber: TEST D/MainActivity: subscriber: TEST 

Sin embargo, la diferencia clave entre usar la nueva técnica es la presencia de una corriente reactiva: Observable . El controlador de texto (o controlador de solicitud en este caso) envía los elementos a la secuencia utilizando el método onNext() . Y Observable tiene suscriptores que procesan estos elementos.


Podemos crear una cadena de métodos antes de suscribirse para reducir la lista de cadenas para el procesamiento. Para empezar, el texto enviado siempre estará en minúsculas y no habrá espacios al principio y al final de la línea:


 Observable.create(ObservableOnSubscribe<String> { ... }) .map { text -> text.toLowerCase().trim() } .subscribe { text -> Log.d(TAG, "subscriber: $text" } 

Corté métodos para mostrar la parte más significativa. Ahora los mismos registros son los siguientes:


 D/MainActivity: subscriber: t D/MainActivity: subscriber: te D/MainActivity: subscriber: tes D/MainActivity: subscriber: test D/MainActivity: subscriber: test 

Ahora apliquemos un retraso de 250 ms, esperando más contenido:


 Observable.create(ObservableOnSubscribe<String> { ... }) .map { text -> text.toLowerCase().trim() } .debounce(250, TimeUnit.MILLISECONDS) .subscribe { text -> Log.d(TAG, "subscriber: $text" } 

Y finalmente, elimine la secuencia duplicada para que solo se procese la primera solicitud única. Las solicitudes idénticas posteriores se ignorarán:


 Observable.create(ObservableOnSubscribe<String> { ... }) .map { text -> text.toLowerCase().trim() } .debounce(100, TimeUnit.MILLISECONDS) .distinct() .subscribe { text -> Log.d(TAG, "subscriber: $text" } 

Nota perev. En este caso, es más razonable usar el operador distinctUntilChanged() , porque de lo contrario, en el caso de una búsqueda repetida en cualquier cadena, la consulta simplemente se ignorará. Y al implementar dicha búsqueda, es razonable prestar atención solo a la última solicitud exitosa e ignorar la nueva si es idéntica a la anterior.

Al final, filtramos las consultas vacías:


 Observable.create(ObservableOnSubscribe<String> { ... }) .map { text -> text.toLowerCase().trim() } .debounce(100, TimeUnit.MILLISECONDS) .distinct() .filter { text -> text.isNotBlank() } .subscribe { text -> Log.d(TAG, "subscriber: $text" } 

En este punto, notará que solo se muestra un mensaje (o quizás dos) en los registros, lo que indica menos llamadas a la API. En este caso, la aplicación continuará funcionando adecuadamente. Además, los casos en los que ingresa algo, pero luego lo elimina e ingresa nuevamente, también generarán menos llamadas a la API.


Hay muchos más operadores diferentes que puede agregar a esta tubería, según sus objetivos. Creo que son muy útiles para trabajar con campos de entrada que interactúan con la API. El código completo es el siguiente:


 // Set up the query listener that executes the search Observable.create(ObservableOnSubscribe<String> { subscriber -> searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener { override fun onQueryTextChange(newText: String?): Boolean { subscriber.onNext(newText!!) return false } override fun onQueryTextSubmit(query: String?): Boolean { subscriber.onNext(query!!) return false } }) }) .map { text -> text.toLowerCase().trim() } .debounce(250, TimeUnit.MILLISECONDS) .distinct() .filter { text -> text.isNotBlank() } .subscribe { text -> Log.d(TAG, "subscriber: $text") } 

Ahora puedo reemplazar el mensaje de registro con una llamada al ViewModel para iniciar una llamada API. Sin embargo, este es un tema para otro artículo.


Conclusión


Usando esta técnica simple para envolver elementos de texto en Observable y usando RxJava, puede reducir la cantidad de llamadas API que se necesitan para realizar operaciones del servidor, así como mejorar la capacidad de respuesta de su aplicación. En este artículo, hemos cubierto solo una pequeña parte de todo el mundo de RxJava, por lo que le dejo enlaces para lecturas adicionales sobre este tema:


  • Dan Lew Grokking RxJava (este es el sitio que me ayudó a moverme en la dirección correcta).
  • Sitio ReactiveX (a menudo me refiero a este sitio cuando construyo una tubería).

Source: https://habr.com/ru/post/es430394/


All Articles