Implementando pesquisa instantânea no Android usando RxJava

Implementando pesquisa instantânea no Android usando RxJava


Estou trabalhando em um novo aplicativo que, como geralmente acontece, se comunica com o serviço de back-end para receber dados por meio da API. Neste exemplo, desenvolverei uma função de pesquisa, um dos recursos que será uma pesquisa instantânea ao inserir texto.


Pesquisa instantânea


Nada complicado, você pensa. Você só precisa colocar o componente de pesquisa na página (provavelmente na barra de ferramentas), conectar o onTextChange eventos onTextChange e realizar a pesquisa. Então aqui está o que eu fiz:


 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) } 

Mas aqui está o problema. Como preciso implementar uma pesquisa diretamente durante a entrada, sempre que um manipulador de eventos onQueryTextChange() , onQueryTextChange() à API para obter o primeiro conjunto de resultados. Os logs são os seguintes:


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

Apesar de estar apenas imprimindo minha solicitação, há cinco chamadas de API, cada uma das quais realiza uma pesquisa. Por exemplo, na nuvem, você precisa pagar por todas as chamadas para a API. Portanto, quando insiro minha solicitação, preciso de um pequeno atraso antes de enviá-la, para que apenas uma chamada de API seja feita no final.


Agora, suponha que eu queira encontrar outra coisa. Excluo TEST e insiro outros 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 

Existem 20 chamadas de API! Um pequeno atraso reduzirá o número dessas chamadas. Também quero me livrar das duplicatas para que o texto recortado não leve a solicitações repetidas. Eu provavelmente também quero filtrar alguns elementos. Por exemplo, você precisa pesquisar sem os caracteres inseridos ou pesquisar por um caractere?


Programação reativa


Existem várias opções para o que fazer em seguida, mas agora quero me concentrar em uma técnica conhecida como programação reativa e a biblioteca RxJava. Quando me deparei com a programação reativa, vi a seguinte descrição:


O ReactiveX é uma API que trabalha com estruturas assíncronas e manipula fluxos de dados ou eventos usando combinações de padrões Observer e Iterator, além de recursos de programação funcional.

Esta definição não explica completamente a natureza e os pontos fortes do ReactiveX. E se isso explica, apenas para aqueles que já estão familiarizados com os princípios dessa estrutura. Eu também vi gráficos assim:


Atraso Operator Chart


O diagrama explica o papel do operador, mas não entende completamente a essência. Então, vamos ver se posso explicar mais claramente esse diagrama com um exemplo simples.


Vamos preparar nosso projeto primeiro. Você precisará de uma nova biblioteca no arquivo build.gradle do seu aplicativo:


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

Lembre-se de sincronizar as dependências do projeto para carregar a biblioteca.


Agora vamos olhar para uma nova solução. Usando o método antigo, acessei a API ao inserir cada novo caractere. Usando o novo método, vou criar um 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 faz exatamente a mesma coisa que o código antigo. Os logs são os seguintes:


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

No entanto, a principal diferença entre o uso da nova técnica é a presença de um fluxo reativo - Observable . O manipulador de texto (ou manipulador de solicitação nesse caso) envia os elementos para o fluxo usando o método onNext() . E o Observable possui assinantes que processam esses elementos.


Podemos criar uma cadeia de métodos antes de assinar para reduzir a lista de seqüências de caracteres para processamento. Para começar, o texto enviado será sempre em minúsculas e não haverá espaços no início e no final da linha:


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

Cortei métodos para mostrar a parte mais significativa. Agora os mesmos logs são os seguintes:


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

Agora vamos aplicar um atraso de 250ms, esperando mais conteúdo:


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

E, finalmente, exclua o fluxo duplicado para que apenas a primeira solicitação exclusiva seja processada. Solicitações idênticas subseqüentes serão ignoradas:


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

Nota perev. Nesse caso, é mais razoável usar o operador distinctUntilChanged() , porque, caso contrário, no caso de pesquisa repetida em qualquer sequência, a consulta será simplesmente ignorada. E ao implementar essa pesquisa, é razoável prestar atenção apenas à última solicitação bem-sucedida e ignorar a nova, se for idêntica à anterior.

No final, filtramos as consultas vazias:


 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" } 

Nesse ponto, você notará que apenas uma (ou talvez duas) mensagens é exibida nos logs, o que indica menos chamadas de API. Nesse caso, o aplicativo continuará funcionando adequadamente. Além disso, os casos em que você digita algo, mas exclui e entra novamente, também resultam em menos chamadas para a API.


Existem muitos outros operadores diferentes que você pode adicionar a esse pipeline, dependendo de seus objetivos. Eu acredito que eles são muito úteis para trabalhar com campos de entrada que interagem com a API. O código completo é o seguinte:


 // 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") } 

Agora eu posso substituir a mensagem de log por uma chamada para o ViewModel para iniciar uma chamada de API. No entanto, este é um tópico para outro artigo.


Conclusão


Usando esta técnica simples para agrupar elementos de texto no Observable e usando RxJava, você pode reduzir o número de chamadas de API necessárias para executar operações do servidor, além de melhorar a capacidade de resposta do seu aplicativo. Neste artigo, abordamos apenas uma pequena parte do mundo inteiro do RxJava, portanto deixo links para leitura adicional sobre este tópico:


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


All Articles