In diesem Codelab lernen Sie, wie Sie Kotlin Coroutinen in einer Android-App verwenden. Das ist eine neue Möglichkeit zur Verwaltung von Hintergrund-Threads, die den Code durch einfachere Callbacks vereinfachen kann. Coroutines ist eine Kotlin-Funktion, die asynchrone Callbacks für lang andauernde Aufgaben wie den Datenbank- oder Netzwerkzugriff in sequenziellen Code umwandelt.
Hier ist ein Code-Snippet, mit dem Sie eine Vorstellung davon erhalten, was Sie tun werden.
// Async callbacks
networkRequest { result ->
// Successful network request
databaseSave(result) { rows ->
// Result saved
}
}
Der Callback-basierte Code wird mithilfe von Koroutinen in sequenziellen Code umgewandelt.
// The same code with coroutines
val result = networkRequest()
// Successful network request
databaseSave(result)
// Result saved
Sie beginnen mit einer vorhandenen App, die mithilfe von Architekturkomponenten erstellt wurde und ein Callback-Format für Aufgaben mit langer Ausführungszeit verwendet.
Nach Abschluss dieses Codelabs hast du genügend Erfahrung, um Koroutinen in deiner App zu verwenden, um Daten aus dem Netzwerk zu laden. Außerdem kannst du Koroutinen in eine App einbinden. Außerdem kennst du dich mit Best Practices für Koroutinen aus und erfährst, wie du einen Test für Code machst, der Koroutinen verwendet.
Voraussetzungen
- Machen Sie sich mit den Architekturkomponenten
ViewModel
,LiveData
,Repository
undRoom
vertraut. - Erfahrung mit Kotlin-Syntax, einschließlich Erweiterungsfunktionen und Lambdas.
- Grundkenntnisse in der Verwendung von Threads unter Android, einschließlich Hauptthread, Hintergrundthreads und Callbacks.
Aufgabe
- Anrufcode, der mit Koroutinen geschrieben und Ergebnisse abgerufen wird
- Asynchronen Code für sequenzielle Aktionen verwenden
- Verwenden Sie
launch
undrunBlocking
, um die Ausführung von Code zu steuern. - Hier erfahren Sie, wie Sie mit
suspendCoroutine
bestehende APIs in Koroutinen konvertieren. - Koroutinen mit Architekturkomponenten verwenden.
- Best Practices zum Testen von Koroutinen
Voraussetzungen
- Android Studio 3.5 (das Codelab funktioniert möglicherweise mit anderen Versionen, allerdings fehlen möglicherweise einige Elemente oder sehen anders aus.)
Sollten während des Codelabs Probleme wie Codefehler, Grammatikfehler oder unklare Formulierungen auftreten, melden Sie es bitte über den Link Fehler melden links unten im Codelab.
Code herunterladen
Klicken Sie auf den folgenden Link, um den gesamten Code für dieses Codelab herunterzuladen:
... oder das GitHub-Repository mit dem folgenden Befehl über die Befehlszeile klonen:
$ git clone https://github.com/googlecodelabs/kotlin-coroutines.git
Häufig gestellte Fragen
Sehen wir uns zuerst an, wie die Beispiel-App beginnt. So kannst du die Beispiel-App in Android Studio öffnen:
- Wenn Sie die ZIP-Datei
kotlin-coroutines
heruntergeladen haben, entpacken Sie die Datei. - Öffne das Projekt
coroutines-codelab
in Android Studio. - Wähle das Anwendungsmodul
start
aus. - Klicken Sie auf die Schaltfläche
Ausführen und wählen Sie entweder einen Emulator oder ein Android-Gerät aus, auf dem Android Lollipop ausgeführt werden kann. Mindestens 21 SDKs werden unterstützt. Der Bildschirm "Kotlin Coroutines" sollte so aussehen:
Diese Starter-App verwendet Threads, um die Anzahl kurz nach dem Drücken des Bildschirms zu erhöhen. Außerdem wird ein neuer Titel aus dem Netzwerk abgerufen und auf dem Bildschirm angezeigt. Probieren Sie es einfach aus. Dadurch sollten sich die Anzahl und die Anzahl der Nachrichten nach einer kurzen Verzögerung ändern. In diesem Codelab wandeln Sie diese Anwendung in Koroutinen um.
Diese App verwendet Architekturkomponenten, um den UI-Code in MainActivity
von der Anwendungslogik in MainViewModel
zu trennen. Nehmen Sie sich einen Moment Zeit, um sich mit der Struktur des Projekts vertraut zu machen.
MainActivity
zeigt die UI an, registriert Klick-Listener und kann eineSnackbar
anzeigen. Er übergibt Ereignisse anMainViewModel
und aktualisiert den Bildschirm basierend aufLiveData
inMainViewModel
.MainViewModel
verarbeitet die Ereignisse inonMainViewClicked
und kommuniziert mitMainActivity
überLiveData.
- Mit
Executors
wird einBACKGROUND,
definiert, der Elemente in einem Hintergrundthread ausführen kann. TitleRepository
ruft Ergebnisse aus dem Netzwerk ab und speichert sie in der Datenbank.
Koroutinen einem Projekt hinzufügen
Damit du Koroutinen in Kotlin verwenden kannst, musst du die coroutines-core
-Bibliothek in die build.gradle (Module: app)
-Datei deines Projekts aufnehmen. Das wurde von den Codelab-Projekten bereits erledigt, du musst dies also nicht tun.
Coroutines unter Android ist als Mediathek und als Erweiterung für Android verfügbar:
- Kotlinx-corountines-core – Hauptoberfläche für die Verwendung von Koroutinen in Kotlin
- kotlinx-coroutines-android: Unterstützung des Android-Haupt-Threads in Koroutinen
Die Starter-App enthält bereits die Abhängigkeiten in build.gradle.
Wenn Sie ein neues App-Projekt erstellen, müssen Sie build.gradle (Module: app)
öffnen und die Koroutinen-Abhängigkeiten dem Projekt hinzufügen.
dependencies { ... implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:x.x.x" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:x.x.x" }
Auf Android-Geräten ist es wichtig, den Haupt-Thread nicht zu blockieren. Der Hauptthread ist ein einzelner Thread, der alle Aktualisierungen der Benutzeroberfläche verarbeitet. Es ist auch der Thread, der alle Klick-Handler und andere UI-Callbacks aufruft. Daher sollte es reibungslos funktionieren.
Damit Ihre App für Nutzer ohne sichtbare Pausen angezeigt wird, muss der Hauptthread den Bildschirm mindestens 16 ms aktualisieren, was ungefähr 60 Bilder pro Sekunde entspricht. Viele häufige Aufgaben dauern länger, z. B. das Parsen großer JSON-Datasets, das Schreiben von Daten in eine Datenbank oder das Abrufen von Daten aus dem Netzwerk. Wenn Sie Code wie diesen aus dem Haupt-Thread aufrufen, kann es sein, dass die App pausiert, ruckelt oder sogar einfriert. Wenn Sie den Haupt-Thread zu lange blockieren, stürzt die App unter Umständen ab und es wird ein Dialogfeld App antwortet nicht angezeigt.
Im folgenden Video erfährst du, wie Koroutinen uns dabei helfen, dieses Problem für Android zu lösen.
Das Callback-Muster
Ein Muster zum Ausführen von Aufgaben mit langer Ausführungszeit, ohne den Hauptthread zu blockieren, sind Callbacks. Mithilfe von Callbacks können Sie lang andauernde Aufgaben in einem Hintergrundthread starten. Wenn die Aufgabe abgeschlossen ist, wird der Callback aufgerufen und so über das Ergebnis im Haupt-Thread informiert.
Hier ein Beispiel für das Callback-Muster.
// Slow request with callbacks
@UiThread
fun makeNetworkRequest() {
// The slow network request runs on another thread
slowFetch { result ->
// When the result is ready, this callback will get the result
show(result)
}
// makeNetworkRequest() exits after calling slowFetch without waiting for the result
}
Da dieser Code mit @UiThread
annotiert ist, muss er schnell genug ausgeführt werden, damit er im Hauptthread ausgeführt werden kann. Das heißt, sie muss sehr schnell zurückkehren, damit sich das nächste Bildschirmupdate nicht verzögert. Da slowFetch
aber nur Sekunden oder sogar Minuten dauert, kann der Haupt-Thread nicht auf das Ergebnis warten. Mit dem Callback show(result)
kann slowFetch
in einem Hintergrundthread ausgeführt und das Ergebnis zurückgegeben werden, sobald es bereit ist.
Rückrufe mithilfe von Koroutinen entfernen
Callbacks sind ein großartiges Muster. Es gibt jedoch auch einige Nachteile. Code, der Callbacks häufig verwendet, kann schwer zu lesen sein und schwerer zu verstehen sein. Außerdem ermöglichen Rückrufe die Verwendung einiger Sprachfunktionen, z. B. Ausnahmen.
Mit Kotlin-Koroutinen können Sie Callback-basierten Code in sequenziellen Code konvertieren. Aufeinanderfolgender Code ist normalerweise leichter zu lesen und es können sogar Sprachfunktionen wie Ausnahmen verwendet werden.
Letztendlich gehen sie genauso vor: Warten Sie, bis ein Ergebnis einer lange laufenden Aufgabe verfügbar ist, und fahren Sie mit der Ausführung fort. Im Code sehen sie jedoch ganz anders aus.
Das Keyword suspend
ist eine Kotlin-Option zum Markieren einer Funktion oder eines Funktionstyps, die für Koroutinen verfügbar ist. Wenn eine Koroutine eine Funktion aufruft, die mit suspend
gekennzeichnet ist, wird die Ausführung statt wie bei einem normalen Funktionsaufruf gesperrt. Bis das Ergebnis fertig ist, wird es gesperrt. Danach wird es an der Stelle fortgesetzt, an der es angehalten wurde. Während sie auf ein Ergebnis wartet, blockiert sie den Thread, auf dem sie ausgeführt wird, damit andere Funktionen oder Koroutinen ausgeführt werden können.
Beispielsweise sind makeNetworkRequest()
und slowFetch()
im Code unten beide suspend
-Funktionen.
// Slow request with coroutines
@UiThread
suspend fun makeNetworkRequest() {
// slowFetch is another suspend function so instead of
// blocking the main thread makeNetworkRequest will `suspend` until the result is
// ready
val result = slowFetch()
// continue to execute after the result is ready
show(result)
}
// slowFetch is main-safe using coroutines
suspend fun slowFetch(): SlowResult { ... }
Genau wie bei der Callback-Version muss makeNetworkRequest
sofort aus dem Haupt-Thread zurückkehren, da @UiThread
als gekennzeichnet ist. Das bedeutet, dass normalerweise keine Blockierungsmethoden wie slowFetch
aufgerufen werden können. Hier funktioniert das Keyword suspend
.
Im Vergleich zum Callback-basierten Code erzielt Coroutine-Code das gleiche Ergebnis wie das Aufheben der Blockierung des aktuellen Threads mit weniger Code. Durch den sequenziellen Stil lassen sich mehrere lang andauernde Aufgaben problemlos verketten, ohne dass mehrere Callbacks erstellt werden. Beispielsweise kann Code, der ein Ergebnis von zwei Netzwerkendpunkten abruft und in der Datenbank speichert, als Funktion in Koroutinen ohne Rückrufe geschrieben werden. Das sieht dann so aus:
// Request data from network and save it to database with coroutines
// Because of the @WorkerThread, this function cannot be called on the
// main thread without causing an error.
@WorkerThread
suspend fun makeNetworkRequest() {
// slowFetch and anotherFetch are suspend functions
val slow = slowFetch()
val another = anotherFetch()
// save is a regular function and will block this thread
database.save(slow, another)
}
// slowFetch is main-safe using coroutines
suspend fun slowFetch(): SlowResult { ... }
// anotherFetch is main-safe using coroutines
suspend fun anotherFetch(): AnotherResult { ... }
Im nächsten Abschnitt führen Sie Koroutinen in der Beispiel-App ein.
In dieser Übung schreiben Sie eine Koroutine, um eine Nachricht nach einer Verzögerung anzuzeigen. Prüfe zuerst, ob das Modul start
in Android Studio geöffnet ist.
Informationen zu CoroutineScope
In Kotlin werden alle Koroutinen in einem CoroutineScope
ausgeführt. Ein Umfang steuert die Lebensdauer von Koroutinen über seinen Job. Wenn Sie den Job eines Bereichs abbrechen, werden alle Koroutinen, die in diesem Bereich gestartet wurden, abgebrochen. Auf Android-Geräten können Sie einen Bereich verwenden, um alle laufenden Koroutinen abzubrechen, wenn beispielsweise der Nutzer eine Activity
oder Fragment
verlässt. Mit Umfangen können Sie auch einen Standardversandplan festlegen. Ein Trafficker bestimmt, welcher Thread eine Koroutine ausführt.
Bei Koroutinen, die über die UI gestartet werden, ist es normalerweise korrekt, sie auf Dispatchers.Main
zu starten. Dies ist der Hauptthread unter Android. Wenn eine Koroutine am Dispatchers.Main
gestartet wurde, wird der Haupt-Thread während der Sperrung nicht blockiert. Da eine ViewModel
-Koroutine die Benutzeroberfläche des Hauptthreads fast immer aktualisiert, werden durch das Starten von Koroutinen im Hauptthread zusätzliche Thread-Switches gespart. Wenn eine Koroutine im Haupt-Thread gestartet wird, kann sie jederzeit nach dem Start des Verteilers gewechselt werden. So lassen sich beispielsweise große JSON-Ergebnisse mit einem anderen Trafficker aus dem Hauptthread parsen.
viewModelScope
Die AndroidX-Bibliothek lifecycle-viewmodel-ktx
fügt ein CoroutineScope zu ViewModels hinzu, die so konfiguriert sind, dass UI-bezogene Koroutinen gestartet werden. Damit Sie diese Bibliothek verwenden können, müssen Sie sie in die Datei build.gradle (Module: start)
Ihres Projekts aufnehmen. Dieser Schritt ist bereits in den Codelab-Projekten abgeschlossen.
dependencies { ... implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:x.x.x" }
Die Bibliothek fügt viewModelScope
als Erweiterungsfunktion der ViewModel
-Klasse hinzu. Dieser Bereich ist an Dispatchers.Main
gebunden und wird automatisch abgebrochen, wenn ViewModel
gelöscht wird.
Von Threads zu Koroutinen wechseln
In MainViewModel.kt
findest du die nächste Aufgabe mit diesem Code:
MainViewModel.kt
/**
* Wait one second then update the tap count.
*/
private fun updateTaps() {
// TODO: Convert updateTaps to use coroutines
tapCount++
BACKGROUND.submit {
Thread.sleep(1_000)
_taps.postValue("$tapCount taps")
}
}
In diesem Code wird die in util/Executor.kt
definierte BACKGROUND ExecutorService
verwendet, um sie in einem Hintergrundthread auszuführen. Da sleep
den aktuellen Thread sperrt, wird die Benutzeroberfläche angehalten, wenn sie im Haupt-Thread aufgerufen wird. Eine Sekunde, nachdem der Nutzer auf die Hauptansicht geklickt hat, wird eine Snackbar angefordert.
Sie können dies erkennen, indem Sie „HINTERGRUND“ aus dem Code entfernen und dann noch einmal ausführen. Das Ladesymbol sorgt dafür, dass alles eine Sekunde später in den endgültigen Zustand rückt.
MainViewModel.kt
/**
* Wait one second then update the tap count.
*/
private fun updateTaps() {
// TODO: Convert updateTaps to use coroutines
tapCount++
Thread.sleep(1_000)
_taps.postValue("$tapCount taps")
}
Ersetzen Sie updateTaps
durch diesen Koroutine-Code, der dasselbe tut. Sie müssen launch
und delay
importieren.
MainViewModel.kt
/**
* Wait one second then display a snackbar.
*/
fun updateTaps() {
// launch a coroutine in viewModelScope
viewModelScope.launch {
tapCount++
// suspend this coroutine for one second
delay(1_000)
// resume in the main dispatcher
// _snackbar.value can be called directly from main thread
_taps.postValue("$tapCount taps")
}
}
Mit diesem Code wird hingegen eine Sekunde gewartet, bevor eine Snackbar angezeigt wird. Es gibt jedoch einige wichtige Unterschiede:
viewModelScope.
launch
beginnt in derviewModelScope
eine Koroutine. Wenn der Job, der anviewModelScope
weitergegeben wurde, abgebrochen wird, werden alle Koroutinen in diesem Job bzw. Bereich gelöscht. Wenn der Nutzer die Aktivität verlassen hat, bevordelay
zurückgegeben wurde, wird diese Koroutine automatisch abgebrochen, wennonCleared
nach dem Löschen des ViewModel aufgerufen wird.- Da
viewModelScope
auf den Standard-Dispatchers.Main
-Fahrt eingestellt ist, wird diese Koroutine im Hauptthread gestartet. Wir werden später darauf eingehen, wie Sie verschiedene Threads verwenden können. - Die Funktion
delay
ist einesuspend
-Funktion. Dies wird in Android Studio durch das Symbollinks im Navigationsbereich angezeigt. Diese Koroutine wird zwar im Hauptthread ausgeführt, aber
delay
wird nicht für eine Sekunde blockiert. Stattdessen wird sie in einer Sekunde bei der nächsten Anweisung fortgesetzt.
Probieren Sie es aus. Wenn Sie auf die Hauptansicht klicken, sollte eine Sekunde später eine Snackbar angezeigt werden.
Im nächsten Abschnitt geht es darum, wie diese Funktion getestet wird.
In dieser Übung schreiben Sie einen Test für den soeben geschriebenen Code. In dieser Übung erfahren Sie, wie Sie Koroutinen, die in Dispatchers.Main
ausgeführt werden, mit der Bibliothek kotlinx-coroutines-test testen. Später in diesem Codelab implementieren Sie einen Test, der direkt mit Koroutinen interagiert.
Vorhandenen Code ansehen
Öffne MainViewModelTest.kt
im Ordner androidTest
.
MainViewModelTest.kt
class MainViewModelTest {
@get:Rule
val coroutineScope = MainCoroutineScopeRule()
@get:Rule
val instantTaskExecutorRule = InstantTaskExecutorRule()
lateinit var subject: MainViewModel
@Before
fun setup() {
subject = MainViewModel(
TitleRepository(
MainNetworkFake("OK"),
TitleDaoFake("initial")
))
}
}
Mit einer Regel können Sie den Code vor und nach der Ausführung eines Tests in JUnit ausführen. Anhand von zwei Regeln können wir MainViewModel in einem Off-Device-Test testen:
InstantTaskExecutorRule
ist eine JUnit-Regel, mit derLiveData
so konfiguriert wird, dass jede Aufgabe synchron ausgeführt wirdMainCoroutineScopeRule
ist eine benutzerdefinierte Regel in dieser Codebasis, dieDispatchers.Main
konfiguriert, um eineTestCoroutineDispatcher
vonkotlinx-coroutines-test
zu verwenden. Dadurch kann eine virtuelle Uhr für Tests verwendet werden und Code kann in EinheitentestsDispatchers.Main
verwenden.
Bei der Methode „setup
“ wird eine neue Instanz von „MainViewModel
“ mithilfe von Testfälschungen erstellt. Das sind gefälschte Implementierungen des Netzwerks und der Datenbank im Startcode. So können Tests ohne Verwendung des echten Netzwerks oder der Datenbank erstellt werden.
Bei diesem Test werden die Fälschungen nur benötigt, um die Abhängigkeiten von MainViewModel
zu erfüllen. Später in diesem Code-Lab aktualisieren Sie die Fälschungen auf Koroutinen.
Einen Test schreiben, der Koroutinen steuert
Fügen Sie einen neuen Test hinzu, mit dem sichergestellt wird, dass Tippen auf die ersten Sekunden nach dem Klicken auf die Hauptansicht aktualisiert wird:
MainViewModelTest.kt
@Test
fun whenMainClicked_updatesTaps() {
subject.onMainViewClicked()
Truth.assertThat(subject.taps.getValueForTest()).isEqualTo("0 taps")
coroutineScope.advanceTimeBy(1000)
Truth.assertThat(subject.taps.getValueForTest()).isEqualTo("1 taps")
}
Durch den Aufruf von onMainViewClicked
wird die soeben erstellte Koroutine gestartet. Bei diesem Test wird geprüft, ob der Text der Berührungen "0-mal tippen direkt nach dem Aufruf der Methode onMainViewClicked
bleibt. Danach wird 1 Sekunde später auf &tapt;1-mal tippen aktualisiert.
Dieser Test verwendet virtuelle Zeit, um die Ausführung der Koroutine von onMainViewClicked
zu steuern. Mit MainCoroutineScopeRule
können Sie Koroutinen, die auf dem Dispatchers.Main
gestartet werden, pausieren, fortsetzen oder steuern. Hier rufen wir advanceTimeBy(1_000)
auf. Dadurch wird der Haupt- aufrufen der Koroutinen, die eine Sekunde später fortgesetzt werden, sofort ausgeführt.
Dieser Test ist vollständig deterministisch, d. h., er wird immer auf die gleiche Weise ausgeführt. Weil die vollständige Kontrolle über die Ausführung von Koroutinen auf der Dispatchers.Main
hat, muss er nicht erst eine Sekunde warten, bis der Wert festgelegt wird.
Vorhandenen Test ausführen
- Klicken Sie in Ihrem Editor mit der rechten Maustaste auf den Kursnamen
MainViewModelTest
, um ein Kontextmenü zu öffnen. - Wählen Sie im Kontextmenü
Ausführen 'MainViewModelTest' aus.
- Für zukünftige Ausführungen können Sie diese Testkonfiguration in den Konfigurationen neben der Schaltfläche
in der Symbolleiste auswählen. Die Konfiguration heißt standardmäßig MainViewModelTest.
Du solltest die Prüfung bestehen. Und das Ganze sollte in weniger als einer Sekunde dauern.
In der nächsten Übung lernen Sie, wie Sie von einer vorhandenen Callback API zu Koroutinen wechseln.
In diesem Schritt beginnen Sie mit der Konvertierung eines Repositories zur Verwendung von Koroutinen. Dazu werden Koroutinen in ViewModel
, Repository
, Room
und Retrofit
hinzugefügt.
Vor der Umstellung auf Koroutinen sollte man sich darüber im Klaren sein, wofür die einzelnen Teile der Architektur verantwortlich sind.
MainDatabase
implementiert eine Datenbank mit Room, die eineTitle
speichert und lädt.MainNetwork
implementiert eine Netzwerk-API, die einen neuen Titel abruft. Er verwendet Retrofit, um Titel abzurufen.Retrofit
ist so konfiguriert, dass sie zufällig Fehler oder Pseudo-Daten zurückgeben, aber ansonsten so funktionieren, als würden sie echte Netzwerkanfragen stellen.TitleRepository
implementiert eine einzelne API zum Abrufen oder Aktualisieren des Titels. Dazu werden Daten aus dem Netzwerk und der Datenbank kombiniert.MainViewModel
stellt den Bildschirmstatus dar und verarbeitet Ereignisse. Dadurch wird dem Repository mitgeteilt, dass der Titel aktualisiert werden soll, wenn der Nutzer auf den Bildschirm tippt.
Da die Netzwerkanfrage durch UI-Ereignisse gesteuert wird und wir eine Koroutine auf Grundlage dieser Ereignisse starten möchten, sollten wir Koroutinen zuerst in ViewModel
verwenden.
Callback-Version
Öffne MainViewModel.kt
, um die Deklaration von refreshTitle
zu sehen.
MainViewModel.kt
/**
* Update title text via this LiveData
*/
val title = repository.title
// ... other code ...
/**
* Refresh the title, showing a loading spinner while it refreshes and errors via snackbar.
*/
fun refreshTitle() {
// TODO: Convert refreshTitle to use coroutines
_spinner.value = true
repository.refreshTitleWithCallbacks(object: TitleRefreshCallback {
override fun onCompleted() {
_spinner.postValue(false)
}
override fun onError(cause: Throwable) {
_snackBar.postValue(cause.message)
_spinner.postValue(false)
}
})
}
Diese Funktion wird jedes Mal aufgerufen, wenn der Nutzer auf den Bildschirm klickt. Das Repository aktualisiert dann den Titel und schreibt den neuen Titel in die Datenbank.
Diese Implementierung verwendet einen Callback, um einige Maßnahmen zu ergreifen:
- Vor dem Starten einer Abfrage wird ein Ladesymbol mit
_spinner.value = true
angezeigt - Wenn ein Ergebnis zurückgegeben wird, wird das Ladesymbol mit
_spinner.value = false
gelöscht - Wenn die Fehlermeldung erscheint, wird eine Snackbar eingeblendet und das Ladesymbol wird gelöscht.
Beachten Sie, dass der Callback onCompleted
nicht an title
übergeben wird. Da wir alle Titel in die Room
-Datenbank schreiben, wird die Benutzeroberfläche auf den aktuellen Titel aktualisiert. Dabei wird ein LiveData
berücksichtigt, die von Room
aktualisiert wurde.
Bei der Aktualisierung der Koroutinen bleibt das Verhalten unverändert. Es empfiehlt sich, eine beobachtbare Datenquelle wie eine Room
-Datenbank zu verwenden, um die UI automatisch auf dem neuesten Stand zu halten.
Die Koroutinenversion
Lass refreshTitle
mit Koroutinen umschreiben.
Da dies sofort erforderlich ist, soll eine leere Funktion zum Sperren in unserem Repository (TitleRespository.kt
) erstellt werden. Definieren Sie eine neue Funktion, die über den Operator suspend
den Kotlin-Code sendet, mit dem Koroutinen verwendet werden können.
TitleRepository.kt
suspend fun refreshTitle() {
// TODO: Refresh from network and write to database
delay(500)
}
Wenn Sie mit diesem Codelab fertig sind, aktualisieren Sie dieses Element, um Retrofit und Room mit einem neuen Titel abzurufen und in Koroutinen in die Datenbank zu schreiben. Aktuell ausgeben sie nur 500 Millisekunden, wenn sie vorgeben, etwas zu tun, und dann fortfahren.
Ersetze in MainViewModel
die Callback-Version von refreshTitle
durch eine, die eine neue Koroutine startet:
MainViewModel.kt
/**
* Refresh the title, showing a loading spinner while it refreshes and errors via snackbar.
*/
fun refreshTitle() {
viewModelScope.launch {
try {
_spinner.value = true
repository.refreshTitle()
} catch (error: TitleRefreshError) {
_snackBar.value = error.message
} finally {
_spinner.value = false
}
}
}
Gehen Sie so vor:
viewModelScope.launch {
Genau wie bei der Koroutine zum Aktualisieren der Anzahl von Klicks musst du auch zuerst eine neue Koroutine in viewModelScope
starten. Hierbei wird Dispatchers.Main
verwendet, was auch kein Problem ist. Obwohl refreshTitle
eine Netzwerkanfrage und eine Datenbankabfrage sendet, können Koroutinen verwendet werden, um eine main-safe-Schnittstelle verfügbar zu machen. Das bedeutet, dass sie aus dem Haupt-Thread aufgerufen werden kann.
Da wir viewModelScope
verwenden, wird der Vorgang, der durch diese Koroutine gestartet wurde, automatisch abgebrochen, wenn der Nutzer den Bildschirm verlässt. Das bedeutet, dass keine zusätzlichen Netzwerk- oder Datenbankabfragen durchgeführt werden.
Mit den nächsten Codezeilen wird refreshTitle
in repository
aufgerufen.
try {
_spinner.value = true
repository.refreshTitle()
}
Bevor diese Koroutine funktioniert, wird das Ladesymbol gestartet – dann wird refreshTitle
wie eine normale Funktion aufgerufen. Da refreshTitle
eine ausgesetzte Funktion ist, wird sie anders ausgeführt als eine normale Funktion.
Wir müssen keinen Rückruf weiterleiten. Die Koroutine wird gesperrt, bis sie von refreshTitle
fortgesetzt wird. Es sieht wie ein Aufruf einer normalen Blockierfunktion aus. Es wird jedoch automatisch gewartet, bis die Netzwerk- und Datenbankabfrage abgeschlossen ist, bevor ohne der Hauptthread blockiert zu werden.
} catch (error: TitleRefreshError) {
_snackBar.value = error.message
} finally {
_spinner.value = false
}
Ausnahmen in Sperrfunktionen funktionieren wie Fehler in regulären Funktionen. Wenn Sie einen Fehler in einer Sperrfunktion auslösen, wird der Aufrufer ausgelöst. Auch wenn sie relativ unterschiedlich ausgeführt werden, können Sie sie mit normalen Try/Catch-Blöcken verarbeiten. Das ist nützlich, weil Sie sich bei der Fehlerbehandlung auf die integrierten Sprachunterstützung verlassen müssen, anstatt für jeden Callback eine benutzerdefinierte Fehlerbehandlung zu erstellen.
Und wenn eine Ausnahme aus einem Koroutine-Ereignis entfernt wird, wird es vom übergeordneten Element standardmäßig abgebrochen. Es ist also ganz einfach, mehrere ähnliche Aufgaben zusammen abzubrechen.
In einem abschließenden Block sorgen wir dafür, dass das Rotorsymbol nach der Ausführung der Abfrage immer ausgeschaltet ist.
Führen Sie die Anwendung noch einmal aus. Wählen Sie dazu die Konfiguration von start aus und drücken Sie dann . Daraufhin sollte ein Ladesymbol angezeigt werden, wenn Sie auf eine beliebige Stelle tippen. Der Titel bleibt unverändert, weil wir unser Netzwerk oder unsere Datenbank noch nicht angeschlossen haben.
In der nächsten Übung aktualisieren Sie das Repository wieder.
In dieser Übung lernen Sie, wie Sie den Thread wechseln, auf dem eine Koroutine ausgeführt wird, um eine funktionierende Version von TitleRepository
zu implementieren.
Vorhandenen Callback-Code in der Aktualisierung „titleTitle“ prüfen
Öffnen Sie TitleRepository.kt
und prüfen Sie die vorhandene Callback-basierte Implementierung.
TitelRepository.kt
// TitleRepository.kt
fun refreshTitleWithCallbacks(titleRefreshCallback: TitleRefreshCallback) {
// This request will be run on a background thread by retrofit
BACKGROUND.submit {
try {
// Make network request using a blocking call
val result = network.fetchNextTitle().execute()
if (result.isSuccessful) {
// Save it to database
titleDao.insertTitle(Title(result.body()!!))
// Inform the caller the refresh is completed
titleRefreshCallback.onCompleted()
} else {
// If it's not successful, inform the callback of the error
titleRefreshCallback.onError(
TitleRefreshError("Unable to refresh title", null))
}
} catch (cause: Throwable) {
// If anything throws an exception, inform the caller
titleRefreshCallback.onError(
TitleRefreshError("Unable to refresh title", cause))
}
}
}
In TitleRepository.kt
wird die Methode refreshTitleWithCallbacks
mit einem Callback implementiert, um den Aufrufer über den Lade- und Fehlerstatus zu informieren.
Damit wird die Aktualisierung ausgeführt.
- Zu einer anderen Unterhaltung mit
BACKGROUND
ExecutorService
wechseln - Führe die
fetchNextTitle
-Netzwerkanfrage mit der blockierendenexecute()
-Methode aus. Dadurch wird die Netzwerkanfrage im aktuellen Thread ausgeführt, in diesem Fall in einem der Threads inBACKGROUND
. - Wenn das Ergebnis erfolgreich ist, speichern Sie es in der Datenbank mit
insertTitle
und rufen Sie die MethodeonCompleted()
auf. - Wenn das Ergebnis nicht erfolgreich war oder es eine Ausnahme gibt, rufen Sie die Methode „onError“ auf und informieren Sie den Anrufer über die fehlgeschlagene Aktualisierung.
Diese Callback-basierte Implementierung ist main-safe, da der Haupt-Thread nicht blockiert wird. Er muss aber einen Callback nutzen, damit der Anrufer informiert wird, wenn die Arbeit abgeschlossen ist. Auch die Callbacks im BACKGROUND
-Thread werden aufgerufen.
Anrufe, die Anrufe über Koroutinen auslösen
Ohne Koroutinen im Netzwerk oder in der Datenbank können wir diesen Code mithilfe von Koroutinen hauptsicher machen. Damit können Sie den Callback entfernen und das Ergebnis an den Thread zurückgeben, der es ursprünglich aufgerufen hat.
Sie können dieses Muster jederzeit verwenden, um Blockierarbeiten oder CPU-intensive Arbeit in einem Koroutin wie das Sortieren und Filtern einer großen Liste oder das Lesen aus dem Laufwerk auszuführen.
Zum Umschalten zwischen allen Dispositionen wird Koroutinen withContext
verwendet. Durch den Aufruf von withContext
wird nur zum Lammda zum anderen Umschalter gewechselt. Danach kehrt er zum Tracker zurück, der es mit dem Ergebnis des Lammda-Elements aufgerufen hat.
Standardmäßig umfasst die Kotlin-Koroutinen drei Trafficker: Main
, IO
und Default
. Der E/A-Trafficker ist für E/A-Vorgänge optimiert, wie z. B. das Lesen aus dem Netzwerk oder Laufwerk. Der Standard-Trafficker ist für CPU-intensive Aufgaben optimiert.
TitelRepository.kt
suspend fun refreshTitle() {
// interact with *blocking* network and IO calls from a coroutine
withContext(Dispatchers.IO) {
val result = try {
// Make network request using a blocking call
network.fetchNextTitle().execute()
} catch (cause: Throwable) {
// If the network throws an exception, inform the caller
throw TitleRefreshError("Unable to refresh title", cause)
}
if (result.isSuccessful) {
// Save it to database
titleDao.insertTitle(Title(result.body()!!))
} else {
// If it's not successful, inform the callback of the error
throw TitleRefreshError("Unable to refresh title", null)
}
}
}
Bei dieser Implementierung werden Blockierungen für das Netzwerk und die Datenbank blockiert, aber es ist dennoch etwas einfacher als die Callback-Version.
Dieser Code verwendet weiterhin blockierende Aufrufe. Durch den Aufruf von execute()
und insertTitle(...)
wird der Thread blockiert, in dem diese Koroutine ausgeführt wird. Durch den Wechsel zu Dispatchers.IO
mit withContext
wird jedoch einer der Threads im Anzeigenauftrag blockiert. Die Koroutine, in der diese URL aufgerufen wurde und möglicherweise auf Dispatchers.Main
ausgeführt wird, wird gesperrt, bis das withContext
-Lambda abgeschlossen ist.
Im Vergleich zur Callback-Version gibt es zwei wichtige Unterschiede:
withContext
gibt das Ergebnis an den Dispatcher zurück, das ihn aufgerufen hat, in diesem FallDispatchers.Main
. Die Callback-Version hat die Callbacks in einem Thread imBACKGROUND
-Executor-Dienst aufgerufen.- Der Anrufer muss keinen Callback an diese Funktion übergeben. Sie können sich dann auf das Sperren und Fortsetzen verlassen, um das Ergebnis oder den Fehler zu erhalten.
App noch einmal ausführen
Wenn Sie die App noch einmal ausführen, sehen Sie, dass die neue Koroutin-basierte Implementierung Ergebnisse aus dem Netzwerk lädt.
Im nächsten Schritt integrieren Sie Koroutinen in Room und Retrofit.
Um die Koroutinen-Integration fortzusetzen, werden wir die Unterstützung für Funktion Sperren in der stabilen Version von Room und Retrofit verwenden und dann den Code, den wir gerade eben geschrieben haben, durch Verwenden der Sperrfunktion vereinfachen.
Koroutinen im Raum
Öffne zuerst MainDatabase.kt
und stell insertTitle
als Sperre ein:
MainDatabase.kt
// add the suspend modifier to the existing insertTitle
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertTitle(title: Title)
In diesem Fall wird Ihre Abfrage in Room main-sicher und automatisch in einem Hintergrundthread ausgeführt. Das bedeutet jedoch auch, dass Sie diese Abfrage nur aus einer Koroutine aufrufen können.
Das wars auch schon. Nicht schlecht.
Koroutinen im Retrostil
Als Nächstes sehen wir uns an, wie Koroutinen in RetroFit integriert werden. Öffnen Sie MainNetwork.kt
und ändern Sie fetchNextTitle
in eine Sperrfunktion.
MainNetwork.kt
// add suspend modifier to the existing fetchNextTitle
// change return type from Call<String> to String
interface MainNetwork {
@GET("next_title.json")
suspend fun fetchNextTitle(): String
}
Wenn Sie Funktion „Sperren“ mit Retrofit verwenden möchten, müssen Sie zwei Dinge tun:
- Funktion „Sperrmodus hinzufügen“ hinzufügen
- Entfernen Sie den
Call
-Wrapper aus dem Rückgabetyp. Hier wirdString
zurückgegeben, aber Sie können auch komplexe JSON-gestützte Typen zurückgeben. Wenn du weiterhin Zugriff auf das vollständigeResult
einrichten möchtest, kannst duResult<String>
stattString
über die Sperrfunktion zurückgeben.
Retrofit sperrt die Funktion zum Anhalten automatisch als sicher, sodass Sie sie direkt in Dispatchers.Main
aufrufen können.
Raum und Retrofit verwenden
Nachdem die Sperrfunktionen von Room und Retrofit jetzt unterstützt werden, können wir sie aus unserem Repository verwenden. Öffnen Sie TitleRepository.kt
und sehen Sie sich an, wie die Sperrfunktion im Vergleich zur Blockierversion die Logik erheblich vereinfacht:
Titel Repository.kt
suspend fun refreshTitle() {
try {
// Make network request using a blocking call
val result = network.fetchNextTitle()
titleDao.insertTitle(Title(result))
} catch (cause: Throwable) {
// If anything throws an exception, inform the caller
throw TitleRefreshError("Unable to refresh title", cause)
}
}
Wow, das ist eine Menge kürzer. Woran liegt das? Wenn Sie die Funktion „Sperren und Fortsetzen“ verwenden, ist der Code wesentlich kürzer. Mit Retrofit können wir hier Rückgabetypen wie String
oder ein User
-Objekt anstelle von Call
verwenden. Dies ist sicher, da die Retrofit
im Rahmen der Sperre die Netzwerkanfrage in einem Hintergrundthread ausführen und die Koroutine fortsetzen kann, wenn der Aufruf abgeschlossen ist.
Außerdem haben wir das withContext
entfernt. Da sowohl Room als auch Retrofit eine sicherste Funktion zum Sperren bereitstellen, ist die Orchestrierung dieser asynchronen Aufgaben von Dispatchers.Main
sicher.
Compiler-Fehler beheben
Beim Wechsel zu Koroutinen wird die Signatur von Funktionen geändert, da eine Funktion zum Anhalten von einer regulären Funktion nicht aufgerufen werden kann. Wenn Sie in diesem Schritt den Modifikator suspend
hinzugefügt haben, wurden einige Compiler-Fehler generiert, die zeigen, was passiert, wenn Sie eine Funktion zum Anhalten in einem echten Projekt ändern.
Sehen Sie sich das Projekt an und beheben Sie die Compiler-Fehler. Ändern Sie dazu die Funktion so, dass die Erstellung unterbrochen wird. Hier sind die jeweiligen Lösungsvorschläge:
Test Testen
Aktualisieren Sie die Testfälschungen, um die neuen Modifikatoren zum Sperren zu unterstützen.
TitleDaofake
- Drücken Sie die ALT-Eingabetaste zum Sperren von Modifikatoren an alle Funktionen in der Hierarchie
MainNetworkfake
- Drücken Sie die ALT-Eingabetaste zum Sperren von Modifikatoren an alle Funktionen in der Hierarchie
- „
fetchNextTitle
“ durch diese Funktion ersetzen
override suspend fun fetchNextTitle() = result
MainNetworkCompletableFake
- Drücken Sie die ALT-Eingabetaste zum Sperren von Modifikatoren an alle Funktionen in der Hierarchie
- „
fetchNextTitle
“ durch diese Funktion ersetzen
override suspend fun fetchNextTitle() = completable.await()
TitelRepository.kt
- Löschen Sie die Funktion
refreshTitleWithCallbacks
, da sie nicht mehr verwendet wird.
App ausführen
Führen Sie die App noch einmal aus, nachdem sie kompiliert wurde. Sie werden sehen, dass sie mit Koroutinen bis zu ViewModell bis Room und Retrofit geladen wird.
Glückwunsch, du hast diese App vollständig durch die Nutzung von Koroutinen ersetzt! Zum Abschluss sprechen wir darüber, wie wir die Leistung gerade getestet haben.
In dieser Übung schreiben Sie einen Test, in dem eine suspend
-Funktion direkt aufgerufen wird.
Da refreshTitle
als öffentliche API bereitgestellt wird, wird sie direkt getestet. So wird gezeigt, wie Koroutinen aus Tests aufgerufen werden.
Hier ist die Funktion refreshTitle
, die Sie in der letzten Übung implementiert haben:
TitelRepository.kt
suspend fun refreshTitle() {
try {
// Make network request using a blocking call
val result = network.fetchNextTitle()
titleDao.insertTitle(Title(result))
} catch (cause: Throwable) {
// If anything throws an exception, inform the caller
throw TitleRefreshError("Unable to refresh title", cause)
}
}
Test schreiben, der eine Sperrfunktion aufruft
Öffnen Sie TitleRepositoryTest.kt
im Ordner „test
“ mit zwei TODOS.
Versuchen Sie, beim ersten whenRefreshTitleSuccess_insertsRows
-Test refreshTitle
aufzurufen.
@Test
fun whenRefreshTitleSuccess_insertsRows() {
val subject = TitleRepository(
MainNetworkFake("OK"),
TitleDaoFake("title")
)
subject.refreshTitle()
}
Da refreshTitle
eine suspend
-Funktion ist, weiß Kotlin, wie sie aufgerufen wird, mit Ausnahme einer Koroutine- oder anderen Sperrfunktion und Sie erhalten einen Compiler-Fehler wie "Funktionsaktualisierung beenden. Diese Funktion sollte nur von einer Koroutine oder einer anderen Sperrfunktion aufgerufen werden.
Der Test-Ausführer weiß nichts über Koroutinen, daher können wir diesen Test nicht zum Anhalten aussetzen. Mit einer CoroutineScope
wie in ViewModel
können wir eine Koroutine mit launch
kombinieren. Allerdings müssen Tests abgeschlossen sein, bevor sie zurückgegeben werden. Sobald eine Testfunktion zurückgegeben wird, ist der Test beendet. Coroutinen, die mit launch
beginnen, sind asynchroner Code. Er kann später abgeschlossen werden. Um diesen asynchronen Code zu testen, müssen Sie eine Möglichkeit bieten, den Test zu warten, bis die Koroutine abgeschlossen ist. Da launch
ein nicht blockierender Aufruf ist, kehrt er sofort zurück und kann nach der Rückgabe der Funktion weiterhin eine Koroutine ausführen. Er kann nicht in Tests verwendet werden. Beispiel:
@Test
fun whenRefreshTitleSuccess_insertsRows() {
val subject = TitleRepository(
MainNetworkFake("OK"),
TitleDaoFake("title")
)
// launch starts a coroutine then immediately returns
GlobalScope.launch {
// since this is asynchronous code, this may be called *after* the test completes
subject.refreshTitle()
}
// test function returns immediately, and
// doesn't see the results of refreshTitle
}
Dieser Test schlägt manchmal fehl. Der Aufruf von launch
wird sofort zurückgegeben und gleichzeitig mit dem Rest des Testlaufs ausgeführt. Der Test kann nicht feststellen, ob refreshTitle
bereits ausgeführt wurde oder nicht. Zusicherungen wie die Überprüfung der Datenbankaktualisierung sind nicht aussagekräftig. Wenn refreshTitle
eine Ausnahme auslöst, wird sie nicht im Testaufrufstack zurückgegeben. Er wird stattdessen in den nicht abgefangenen Ausnahme-Handler von GlobalScope
verworfen.
Die Bibliothek kotlinx-coroutines-test
hat die Funktion runBlockingTest
, die Sperrfunktionen aufruft, während sie aufgerufen wird. Wenn runBlockingTest
eine Sperrfunktion oder launches
eine neue Koroutine aufruft, wird diese standardmäßig ausgeführt. Stellen Sie sich die Funktion also als eine Möglichkeit vor, Sperrfunktionen und Koroutinen in normale Funktionsaufrufe umzuwandeln.
Außerdem werden von runBlockingTest
nicht abgefangene Ausnahmen aufgehoben. So können Sie noch einfacher testen, wenn eine Koroutine eine Ausnahme auslöst.
Test mit einer Koroutine durchführen
Sie können den Aufruf in refreshTitle
mit runBlockingTest
umschließen und den Wrapper GlobalScope.launch
aus subject.refreshTitle() entfernen.
TitelRepositoryTest.kt
@Test
fun whenRefreshTitleSuccess_insertsRows() = runBlockingTest {
val titleDao = TitleDaoFake("title")
val subject = TitleRepository(
MainNetworkFake("OK"),
titleDao
)
subject.refreshTitle()
Truth.assertThat(titleDao.nextInsertedOrNull()).isEqualTo("OK")
}
Bei dieser Prüfung wird mithilfe der Fälschungen geprüft, ob „OK“ durch refreshTitle
in die Datenbank eingefügt wurde.
Wenn der Test runBlockingTest
aufruft, wird er blockiert, bis die Koroutine von runBlockingTest
gestartet wurde. Wenn wir dann „refreshTitle
“ aufrufen, wird der reguläre Mechanismus zum Anhalten und Fortsetzen verwendet, um zu warten, bis die Datenbankzeile der Fake hinzugefügt wurde.
Nach Abschluss der Testkoroutine wird runBlockingTest
zurückgegeben.
Zeitüberschreitungstest schreiben
Wir möchten der Netzwerkanfrage ein kurzes Zeitlimit hinzufügen. Zuerst schreiben Sie den Test und implementieren dann das Zeitlimit. So erstellen Sie einen neuen Test:
TitelRepositoryTest.kt
@Test(expected = TitleRefreshError::class)
fun whenRefreshTitleTimeout_throws() = runBlockingTest {
val network = MainNetworkCompletableFake()
val subject = TitleRepository(
network,
TitleDaoFake("title")
)
launch {
subject.refreshTitle()
}
advanceTimeBy(5_000)
}
Bei diesem Test wird die bereitgestellte Fake-MainNetworkCompletableFake
verwendet. Dabei handelt es sich um eine Netzwerkfälschung, mit der Anrufer gesperrt werden, bis der Test fortgesetzt wird. Wenn refreshTitle
versucht, eine Netzwerkanfrage zu senden, bleibt er hängen, da wir Zeitüberschreitungen testen möchten.
Anschließend wird eine separate Koroutine zum Aufrufen von refreshTitle
gestartet. Das ist ein wichtiger Teil von Zeitüberschreitungen beim Testen. Das Zeitlimit sollte in einer anderen Koroutine angewendet werden, als die von runBlockingTest
erstellt wird. Dadurch können wir die nächste Zeile advanceTimeBy(5_000)
aufrufen, die die Zeit um fünf Sekunden erhöht und die andere Koroutine beeinflusst.
Das ist ein vollständiges Zeitlimittest, das bestanden wird, sobald das Zeitlimit implementiert wird.
Führen Sie den Befehl aus und prüfen Sie, was passiert:
Caused by: kotlinx.coroutines.test.UncompletedCoroutinesError: Test finished with active jobs: ["...]
Eine der Funktionen von runBlockingTest
ist, dass damit Koroutinen nach Abschluss des Tests nicht zugänglich sind. Wenn nicht fertiggestellte Koroutinen vorhanden sind, z. B. unsere Koroutine am Ende des Tests, schlägt der Test fehl.
Zeitüberschreitung hinzufügen
Öffnen Sie TitleRepository
und fügen Sie dem Netzwerkabruf ein Zeitlimit von fünf Sekunden hinzu. Verwenden Sie dazu die Funktion withTimeout
:
TitelRepository.kt
suspend fun refreshTitle() {
try {
// Make network request using a blocking call
val result = withTimeout(5_000) {
network.fetchNextTitle()
}
titleDao.insertTitle(Title(result))
} catch (cause: Throwable) {
// If anything throws an exception, inform the caller
throw TitleRefreshError("Unable to refresh title", cause)
}
}
Test ausführen Wenn Sie die Tests ausführen, sollten alle Tests bestanden werden.
In der nächsten Übung erfahren Sie, wie Sie mithilfe von Koroutinen Funktionen mit höherer Reihenfolge schreiben.
In dieser Übung refaktorieren Sie refreshTitle
in MainViewModel
für die Verwendung einer allgemeinen Funktion zum Laden von Daten. So erfahren Sie, wie Sie höherwertige Funktionen erstellen, die Koroutinen verwenden.
Die aktuelle Implementierung von refreshTitle
funktioniert, aber wir können eine allgemeine Datenlademethode erstellen, die immer das Ladesymbol anzeigt. Das kann in einer Codebasis hilfreich sein, die Daten als Reaktion auf mehrere Ereignisse lädt und dafür sorgt, dass das Ladesymbol konsistent angezeigt wird.
Wenn Sie die aktuelle Implementierung in jeder Zeile mit Ausnahme von repository.refreshTitle()
prüfen, sind Textbausteine die Spin- und Anzeigefehler.
// MainViewModel.kt
fun refreshTitle() {
viewModelScope.launch {
try {
_spinner.value = true
// this is the only part that changes between sources
repository.refreshTitle()
} catch (error: TitleRefreshError) {
_snackBar.value = error.message
} finally {
_spinner.value = false
}
}
}
Koroutinen in höheren Reihenfolgen verwenden
Fügen Sie diesen Code zu MainViewModel.kt hinzu.
MainViewModel.kt
private fun launchDataLoad(block: suspend () -> Unit): Job {
return viewModelScope.launch {
try {
_spinner.value = true
block()
} catch (error: TitleRefreshError) {
_snackBar.value = error.message
} finally {
_spinner.value = false
}
}
}
Refaktorieren Sie refreshTitle()
, um die Funktion mit höherer Reihenfolge zu nutzen.
MainViewModel.kt
fun refreshTitle() {
launchDataLoad {
repository.refreshTitle()
}
}
Da wir abstrahieren, wie die Logik für das Laden der Ladefläche und das Anzeigen von Fehlern präsentiert werden, haben wir den Code für das Laden der Daten vereinfacht. Beim Rotieren eines Kreises oder beim Anzeigen eines Fehlers kann das allgemeine Laden von Daten leicht verallgemeinert werden, während die tatsächliche Datenquelle und das Ziel jedes Mal angegeben werden müssen.
Zum Erstellen dieser Abstraktion verwendet launchDataLoad
das Argument block
, das ein gesperrtes Lambda ist. Mit einem Lambda-Sperren können Sie Sperrfunktionen aufrufen. So werden die Koroutine-Builder launch
und runBlocking
, die wir in diesem Codelab verwendet haben, von Kotlin implementiert.
// suspend lambda
block: suspend () -> Unit
Wenn Sie eine Lammbda sperren möchten, beginnen Sie mit dem Keyword suspend
. Die Funktionsdeklaration wird mit dem Funktionspfeil und dem Rückgabetyp Unit
abgeschlossen.
Oft müssen Sie keine eigenen Lammdas deklarieren, aber es kann hilfreich sein, Abstraktionen wie diese zu erstellen, die sich durch wiederholte Logik verbinden.
In dieser Übung lernen Sie, wie Sie Koroutine-basierten Code aus WorkManager verwenden.
Was ist WorkManager?
Auf Android-Geräten stehen viele Optionen zur Verfügung, um die Bearbeitung im Hintergrund zu ermöglichen. In dieser Übung erfahren Sie, wie Sie WorkManager in Koroutinen integrieren. WorkManager ist eine kompatible, flexible und einfache Bibliothek für Hintergrundarbeiten. Für diese Anwendungsfälle unter Android wird WorkManager empfohlen.
WorkManager ist Teil von Android Jetpack und einer Architekturkomponente, die eine Kombination aus Opportunity- und garantierter Ausführung bietet. Opportunistische Ausführung bedeutet, dass WorkManager Ihre Hintergrundarbeit so bald wie möglich erledigt. Garantierte Ausführung bedeutet, dass WorkManager die Logik für den Start Ihrer Arbeit in verschiedenen Situationen übernimmt, auch wenn Sie die App verlassen.
Aus diesem Grund ist WorkManager eine gute Wahl für Aufgaben, die irgendwann abgeschlossen werden müssen.
Einige Beispiele für Aufgaben, die sich für den Einsatz mit WorkManager eignen:
- Logs werden hochgeladen
- Filter auf Bilder anwenden und Bild speichern
- Regelmäßige Synchronisierung lokaler Daten mit dem Netzwerk
Koroutinen mit WorkManager verwenden
WorkManager bietet verschiedene Implementierungen der Basis-Klasse ListanableWorker
für verschiedene Anwendungsfälle.
Bei der einfachsten Worker-Klasse können einige synchrone Vorgänge von WorkManager ausgeführt werden. Da wir unsere Codebasis jedoch bisher für die Verwendung von Koroutinen und Sperrfunktionen konvertiert haben, funktioniert WorkManager am besten mit der Klasse CoroutineWorker
, mit der die Funktion doWork()
als Sperrfunktion definiert werden kann.
Öffne RefreshMainDataWork
, um loszulegen. Sie erweitert CoroutineWorker
bereits und Sie müssen doWork
implementieren.
Rufen Sie in der suspend
doWork
-Funktion refreshTitle()
aus dem Repository auf und geben Sie das entsprechende Ergebnis zurück.
Nachdem Sie die TODO-Aufgabe abgeschlossen haben, sieht der Code so aus:
override suspend fun doWork(): Result {
val database = getDatabase(applicationContext)
val repository = TitleRepository(network, database.titleDao)
return try {
repository.refreshTitle()
Result.success()
} catch (error: TitleRefreshError) {
Result.failure()
}
}
CoroutineWorker.doWork()
ist eine ausgesetzte Funktion. Im Gegensatz zur einfacheren Worker
-Klasse wird dieser Code NICHT auf dem in der WorkManager-Konfiguration angegebenen Executor ausgeführt, sondern stattdessen die Disposition in coroutineContext
verwendet (standardmäßig Dispatchers.Default
).
Unser CoroutineWorker testen
Keine Codebasis sollte ohne Tests vollständig sein.
WorkManager bietet verschiedene Möglichkeiten, Ihre Worker
-Kurse zu testen. In der Dokumentation erfahren Sie mehr über die ursprüngliche Testinfrastruktur.
Mit Version 2.1 des WorkManagers werden neue APIs eingeführt, um das Testen von ListenableWorker
-Klassen und somit auch CoroutineWorker einfacher zu ermöglichen. In unserem Code verwenden wir eine dieser neuen APIs: TestListenableWorkerBuilder
.
Wenn du unseren neuen Test hinzufügen möchtest, aktualisiere die Datei RefreshMainDataWorkTest
im Ordner androidTest
.
Der Inhalt der Datei ist:
package com.example.android.kotlincoroutines.main
import android.content.Context
import androidx.test.core.app.ApplicationProvider
import androidx.work.ListenableWorker.Result
import android.content.Context
import androidx.test.core.app.ApplicationProvider
import androidx.work.ListenableWorker.Result
import androidx.work.testing.TestListenableWorkerBuilder
import com.example.android.kotlincoroutines.fakes.MainNetworkFake
import com.google.common.truth.Truth.assertThat
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
@RunWith(JUnit4::class)
class RefreshMainDataWorkTest {
@Test
fun testRefreshMainDataWork() {
val fakeNetwork = MainNetworkFake("OK")
val context = ApplicationProvider.getApplicationContext<Context>()
val worker = TestListenableWorkerBuilder<RefreshMainDataWork>(context)
.setWorkerFactory(RefreshMainDataWork.Factory(fakeNetwork))
.build()
// Start the work synchronously
val result = worker.startWork().get()
assertThat(result).isEqualTo(Result.success())
}
}
Bevor wir zum Test kommen, informieren wir WorkManager
über das Werk, damit wir das falsche Netzwerk einschleusen können.
Der Test selbst erstellt TestListenableWorkerBuilder
mit dem Worker, den wir dann aufrufen können, um die Methode startWork()
aufzurufen.
WorkManager ist nur ein Beispiel dafür, wie Koroutinen dazu verwendet werden können, das API-Design zu vereinfachen.
In diesem Codelab wurden die Grundlagen behandelt. Jetzt musst du Koroutinen in deiner App verwenden.
Dabei ging es um folgende Themen:
- Koroutinen über die Benutzeroberfläche und über WorkManager-Jobs in Android-Apps einbinden, um die asynchrone Programmierung zu vereinfachen
- Koroutinen in einem
ViewModel
verwenden, um Daten aus dem Netzwerk abzurufen und in einer Datenbank zu speichern, ohne den Hauptthread zu blockieren. - Wie du alle Koroutinen beendest, wenn
ViewModel
beendet ist.
Zum Testen von Koroutine-Code wurden sowohl das Testverhalten als auch das direkte Aufrufen von suspend
-Funktionen aus Tests behandelt.
Weitere Informationen
In unserem Codelab Erweiterte Koroutinen mit Kotlin-Datenfluss und LiveData finden Sie weitere Informationen zur Nutzung der Koroutinen unter Android.
Kotlin-Koroutinen haben viele Funktionen, die von diesem Codelab nicht abgedeckt wurden. Weitere Informationen zu Koroutinen von Kotlin finden Sie in den Coroutinen-Leitfäden von JetBrains. Unter Apps mit Kotlin-Koroutinen verbessern finden Sie weitere Nutzungsmuster von Koroutinen unter Android.