Sofortige Android-Suche mit RxJava implementieren

Ursprünglicher Autor: Adrian Hall
  • Übersetzung

Sofortige Android-Suche mit RxJava implementieren


Ich arbeite an einer neuen Anwendung, die wie üblich mit dem Backend-Dienst kommuniziert, um Daten über die API abzurufen. In diesem Beispiel werde ich eine Suchfunktion entwickeln. Eine der Funktionen ist eine sofortige Suche direkt beim Tippen.


Sofortige Suche


Nichts kompliziertes, denkst du. Sie müssen lediglich die Suchkomponente auf der Seite platzieren (höchstwahrscheinlich in der Symbolleiste), einen Event-Handler anschließen onTextChangeund eine Suche durchführen. Also, was ich getan habe:


overridefunonCreateOptionsMenu(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 {
        overridefunonQueryTextSubmit(query: String?): Boolean {
            Log.d(TAG, "onQueryTextSubmit: $query")
            returnfalse
        }
        overridefunonQueryTextChange(newText: String?): Boolean {
            Log.d(TAG, "onQueryTextChange: $newText")
            returnfalse
        }
    })
    returnsuper.onCreateOptionsMenu(menu)
}

Aber das ist das Problem. Da ich während der Eingabe eine Suche direkt implementieren muss onQueryTextChange(), rufe ich immer dann die API auf, wenn ein Event-Handler ausgelöst wird , um die ersten Ergebnisse zu erhalten. Protokolle lauten wie folgt:


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

Trotz der Tatsache, dass ich gerade meine Anfrage eingebe, gibt es fünf API-Aufrufe, von denen jeder eine Suche durchführt. In der Cloud müssen Sie beispielsweise für jeden API-Aufruf bezahlen. Wenn ich also meine Anfrage eingebe, brauche ich eine kurze Verzögerung, um sie zu senden, sodass nur ein Aufruf der API erfolgt.


Nehmen wir an, ich möchte etwas anderes finden. Ich lösche TEST und gebe andere Zeichen ein:


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

Es treten 20 API-Aufrufe auf! Eine kleine Verzögerung verringert die Anzahl dieser Anrufe. Ich möchte auch Duplikate loswerden, damit der getrimmte Text nicht zu wiederholten Abfragen führt. Ich möchte wahrscheinlich auch einige Elemente herausfiltern. Benötigen Sie zum Beispiel die Möglichkeit zu suchen, ohne Zeichen einzugeben oder nach einem einzelnen Zeichen zu suchen?


Reaktive Programmierung


Es gibt mehrere Optionen für weitere Maßnahmen, aber im Moment möchte ich mich auf die Technik konzentrieren, die allgemein als reaktive Programmierung und die RxJava-Bibliothek bekannt ist. Als ich zum ersten Mal reaktive Programmierung sah, sah ich die folgende Beschreibung:


ReactiveX ist eine API, die mit asynchronen Strukturen arbeitet und Datenströme oder Ereignisse mithilfe von Kombinationen von Observer- und Iterator-Mustern sowie Funktionen der funktionalen Programmierung manipuliert.

Diese Definition erklärt das Wesen und die Stärken von ReactiveX nicht vollständig. Und wenn es erklärt, nur für diejenigen, die bereits mit den Prinzipien der Arbeit dieses Rahmens vertraut sind. Ich habe auch solche Diagramme gesehen:


Verzögerungstabelle


Das Diagramm erläutert die Rolle des Bedieners, erlaubt jedoch nicht das vollständige Verständnis der Essenz. Mal sehen, ob ich dieses Diagramm anhand eines einfachen Beispiels deutlicher erklären kann.


Lassen Sie uns zuerst unser Projekt vorbereiten. Sie benötigen eine neue Bibliothek in build.gradleIhrer Anwendungsdatei:


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

Denken Sie daran, Projektabhängigkeiten zu synchronisieren, um die Bibliothek zu laden.


Betrachten wir nun eine neue Lösung. Mit der alten Methode habe ich bei der Eingabe jedes neuen Zeichens auf die API zugegriffen. Mit Hilfe eines neuen Weges werde ich Folgendes schaffen Observable:


overridefunonCreateOptionsMenu(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 {
            overridefunonQueryTextChange(newText: String?): Boolean {
                subscriber.onNext(newText!!)
                returnfalse
            }
            overridefunonQueryTextSubmit(query: String?): Boolean {
                subscriber.onNext(query!!)
                returnfalse
            }
        })
    })
    .subscribe { text ->
        Log.d(TAG, "subscriber: $text")
    }
    returnsuper.onCreateOptionsMenu(menu)
}

Dieser Code entspricht genau dem alten Code. Protokolle lauten wie folgt:


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

Der Hauptunterschied bei der Verwendung einer neuen Technik ist jedoch das Vorhandensein eines Düsenstrahls Observable. Der Texthandler (oder in diesem Fall der Anforderungshandler) sendet die Elemente mithilfe einer Methode an den Stream onNext(). Und Observablees gibt Abonnenten, die mit diesen Elementen umgehen.


Wir können vor dem Abonnieren eine Kette von Methoden erstellen, um die Liste der zu verarbeitenden Strings zu reduzieren. Beginnen wir mit der Tatsache, dass der gesendete Text immer in Kleinbuchstaben geschrieben wird und am Anfang und Ende der Zeile keine Leerzeichen stehen:


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

Ich habe die Methoden verkürzt, um den wichtigsten Teil darzustellen. Nun sehen die gleichen Protokolle so aus:


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

Lassen Sie uns nun eine Verzögerung von 250 ms anwenden und erwarten Sie mehr Inhalt:


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

Und schließlich werden wir doppelte Streams entfernen, sodass nur die erste eindeutige Anforderung verarbeitet wird. Nachfolgende identische Anfragen werden ignoriert:


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

Hinweis trans. In diesem Fall ist es sinnvoller, einen Operator zu verwenden distinctUntilChanged(), da sonst bei einer wiederholten Suche nach einer Zeichenfolge die Anforderung einfach ignoriert wird. Bei der Durchführung einer solchen Suche ist es sinnvoll, nur die letzte erfolgreiche Anfrage zu berücksichtigen und die neue Anfrage zu ignorieren, wenn sie mit der vorherigen identisch ist.

Zum Schluss filtern wir leere Abfragen:


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

In diesem Stadium werden Sie feststellen, dass nur eine (oder möglicherweise zwei) Meldungen in den Protokollen angezeigt werden, was weniger API-Aufrufe bedeutet. Gleichzeitig funktioniert die Anwendung weiterhin ordnungsgemäß. Darüber hinaus führen Fälle, in denen Sie etwas eingeben, dann aber löschen und erneut eingeben, zu weniger API-Aufrufen.


Es gibt viele weitere verschiedene Operatoren, die Sie je nach Ihren Zielen zu dieser Pipeline hinzufügen können. Ich finde, dass sie sehr nützlich sind, um mit Eingabefeldern zu arbeiten, die mit der API interagieren. Der vollständige Code sieht folgendermaßen aus:


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

Jetzt kann ich die Protokollnachricht durch einen Aufruf an ViewModel ersetzen, um einen API-Aufruf zu initiieren. Dies ist jedoch ein Thema für einen anderen Artikel.


Fazit


Mit dieser einfachen Umhüllungstechnik für Textelemente in Observableund mit RxJava können Sie die Anzahl der für Serveroperationen erforderlichen API-Aufrufe reduzieren und die Reaktionsfähigkeit Ihrer Anwendung verbessern. In diesem Artikel haben wir nur einen kleinen Teil der gesamten Welt von RxJava behandelt. Ich lasse Ihnen Links zu weiteren Informationen zu diesem Thema:


  • Grokking RxJava von Dan Lew (diese Seite hat mir geholfen, mich in die richtige Richtung zu bewegen).
  • ReactiveX-Site (ich beziehe mich beim Bauen der Pipelines häufig auf diese Site).

Jetzt auch beliebt: