Implémentation de la recherche instantanée dans Android à l'aide de RxJava

Implémentation de la recherche instantanée dans Android à l'aide de RxJava


Je travaille sur une nouvelle application qui, comme d'habitude, communique avec le service backend pour recevoir des données via l'API. Dans cet exemple, je développerai une fonction de recherche, dont l'une des fonctionnalités sera un droit de recherche instantanée lors de la saisie de texte.


Recherche instantanée


Rien de compliqué, vous pensez. Il vous suffit de placer le composant de recherche sur la page (très probablement, dans la barre d'outils), de connecter le onTextChange événements onTextChange et d'effectuer la recherche. Voici donc ce que j'ai fait:


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

Mais voici le problème. Étant donné que je dois implémenter une recherche directement lors de la saisie, chaque fois qu'un gestionnaire d'événements onQueryTextChange() , je me tourne vers l'API pour obtenir le premier ensemble de résultats. Les journaux sont les suivants:


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

Malgré le fait que j'imprime simplement ma demande, il existe cinq appels d'API, chacun effectuant une recherche. Par exemple, dans le cloud, vous devez payer pour chaque appel à l'API. Ainsi, lorsque j'entre ma demande, j'ai besoin d'un peu de temps avant de l'envoyer, pour qu'un seul appel API soit finalement fait.


Supposons maintenant que je veuille trouver autre chose. Je supprime TEST et saisis d'autres caractères:


 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 

Il y a 20 appels API! Un petit retard réduira le nombre de ces appels. Je veux également me débarrasser des doublons afin que le texte recadré ne conduise pas à des requêtes répétées. Je veux aussi probablement filtrer certains éléments. Par exemple, avez-vous besoin de la possibilité de rechercher sans les caractères saisis ou de rechercher par un seul caractère?


Programmation réactive


Il y a plusieurs options pour que faire ensuite, mais en ce moment je veux me concentrer sur une technique qui est communément connue sous le nom de programmation réactive et de la bibliothèque RxJava. Quand j'ai rencontré la programmation réactive pour la première fois, j'ai vu la description suivante:


ReactiveX est une API qui fonctionne avec des structures asynchrones et manipule des flux de données ou des événements à l'aide de combinaisons de modèles Observer et Iterator, ainsi que des fonctionnalités de programmation fonctionnelles.

Cette définition n'explique pas entièrement la nature et les points forts de ReactiveX. Et si cela explique, alors seulement à ceux qui connaissent déjà les principes de ce cadre. J'ai également vu des graphiques comme celui-ci:


Tableau des opérateurs de retard


Le diagramme explique le rôle de l'opérateur, mais ne comprend pas complètement l'essence. Voyons donc si je peux expliquer plus clairement ce diagramme avec un exemple simple.


Préparons d'abord notre projet. Vous aurez besoin d'une nouvelle bibliothèque dans le fichier build.gradle de votre application:


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

N'oubliez pas de synchroniser les dépendances du projet pour charger la bibliothèque.


Voyons maintenant une nouvelle solution. En utilisant l'ancienne méthode, j'ai accédé à l'API en entrant chaque nouveau caractère. En utilisant la nouvelle méthode, je vais créer 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) } 

Ce code fait exactement la même chose que l'ancien code. Les journaux sont les suivants:


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

Cependant, la principale différence entre l'utilisation de la nouvelle technique est la présence d'un flux réactif - Observable . Le gestionnaire de texte (ou le gestionnaire de requêtes dans ce cas) envoie les éléments au flux à l'aide de la méthode onNext() . Et Observable a des abonnés qui traitent ces éléments.


Nous pouvons créer une chaîne de méthodes avant de vous abonner afin de réduire la liste des chaînes à traiter. Pour commencer, le texte envoyé sera toujours en minuscules et il n'y aura pas d'espaces au début et à la fin de la ligne:


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

J'ai coupé des méthodes pour montrer la partie la plus importante. Maintenant, les mêmes journaux sont les suivants:


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

Appliquons maintenant un délai de 250 ms, en attendant plus de contenu:


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

Et enfin, supprimez le flux en double afin que seule la première demande unique soit traitée. Les demandes identiques suivantes seront ignorées:


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

Remarque perev. Dans ce cas, il est plus raisonnable d'utiliser l'opérateur distinctUntilChanged() , car sinon, dans le cas d'une recherche répétée sur n'importe quelle chaîne, la requête sera simplement ignorée. Et lors de la mise en œuvre d'une telle recherche, il est raisonnable de ne prêter attention qu'à la dernière requête réussie et d'ignorer la nouvelle si elle est identique à la précédente.

À la fin, nous filtrons les requêtes vides:


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

À ce stade, vous remarquerez qu'un seul (ou peut-être deux) message est affiché dans les journaux, ce qui indique moins d'appels d'API. Dans ce cas, l'application continuera de fonctionner correctement. De plus, les cas où vous entrez quelque chose, mais que vous supprimez puis saisissez à nouveau, entraîneront également moins d'appels à l'API.


Il existe de nombreux opérateurs différents que vous pouvez ajouter à ce pipeline, en fonction de vos objectifs. Je pense qu'ils sont très utiles pour travailler avec des champs de saisie qui interagissent avec l'API. Le code complet est le suivant:


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

Maintenant, je peux remplacer le message du journal par un appel au ViewModel pour lancer un appel API. Cependant, c'est un sujet pour un autre article.


Conclusion


En utilisant cette technique simple pour encapsuler des éléments de texte dans Observable et en utilisant RxJava, vous pouvez réduire le nombre d'appels d'API nécessaires pour effectuer des opérations sur le serveur, ainsi que pour améliorer la réactivité de votre application. Dans cet article, nous n'avons couvert qu'une petite partie du monde entier de RxJava, donc je vous laisse des liens pour une lecture supplémentaire sur ce sujet:


  • Dan Lew Grokking RxJava (c'est le site qui m'a aidé à aller dans la bonne direction).
  • Site ReactiveX (je me réfère souvent à ce site lors de la construction d'un pipeline).

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


All Articles