In diesem Codelab erfahren Sie, wie Sie Kotlin-Coroutinen in einer Android-App verwenden. Das ist eine neue Methode zum Verwalten von Hintergrundthreads, die den Code vereinfachen kann, da weniger Callbacks erforderlich sind. Coroutinen sind eine Kotlin-Funktion, mit der asynchrone Callbacks für lang andauernde Aufgaben wie Datenbank- oder Netzwerkzugriff in sequenziellen Code umgewandelt werden.
Hier ist ein Code-Snippet, das Ihnen eine Vorstellung davon vermittelt, was Sie tun müssen.
// Async callbacks
networkRequest { result ->
// Successful network request
databaseSave(result) { rows ->
// Result saved
}
}
Der auf Callbacks basierende Code wird mithilfe von Coroutinen in sequenziellen Code konvertiert.
// The same code with coroutines
val result = networkRequest()
// Successful network request
databaseSave(result)
// Result saved
Sie beginnen mit einer vorhandenen App, die mit Architekturkomponenten erstellt wurde und für lang andauernde Aufgaben einen Callback-Stil verwendet.
Am Ende dieses Codelabs haben Sie genug Erfahrung, um Coroutinen in Ihrer App zu verwenden, um Daten aus dem Netzwerk zu laden. Außerdem wissen Sie, wie Sie Coroutinen in eine App einbinden, kennen die Best Practices für Coroutinen und wissen, wie Sie einen Test für Code schreiben, der Coroutinen verwendet.
Voraussetzungen
- Vertrautheit mit den Architekturkomponenten
ViewModel
,LiveData
,Repository
undRoom
. - Erfahrung mit der Kotlin-Syntax, einschließlich Erweiterungsfunktionen und Lambdas.
- Grundlegendes Verständnis der Verwendung von Threads unter Android, einschließlich des Hauptthreads, von Hintergrundthreads und von Callbacks.
Aufgabe
- Mit Coroutinen geschriebenen Code aufrufen und Ergebnisse abrufen.
- Verwenden Sie suspend-Funktionen, um asynchronen Code sequenziell zu machen.
- Mit
launch
undrunBlocking
können Sie steuern, wie Code ausgeführt wird. - Hier erfahren Sie, wie Sie vorhandene APIs mit
suspendCoroutine
in Coroutinen umwandeln. - Coroutinen mit Architecture Components verwenden
- Best Practices für das Testen von Coroutinen
Voraussetzungen
- Android Studio 3.5 (das Codelab funktioniert möglicherweise auch mit anderen Versionen, aber einige Dinge fehlen möglicherweise oder sehen anders aus).
Wenn Sie bei der Bearbeitung dieses Codelabs auf Probleme stoßen (z. B. Codefehler, Grammatikfehler oder unklare Formulierungen), melden Sie das Problem bitte über den Link Fehler melden unten links im Codelab.
Code herunterladen
Klicken Sie auf den folgenden Link, um den gesamten Code für dieses Codelab herunterzuladen:
… oder klonen Sie das GitHub-Repository über die Befehlszeile mit dem folgenden Befehl:
$ git clone https://github.com/googlecodelabs/kotlin-coroutines.git
Häufig gestellte Fragen
Sehen wir uns zuerst die Beispiel-App an. Folgen Sie dieser Anleitung, um die Beispiel-App in Android Studio zu öffnen.
- Wenn Sie die ZIP-Datei
kotlin-coroutines
heruntergeladen haben, entpacken Sie sie. - Öffnen Sie das
coroutines-codelab
-Projekt in Android Studio. - Wählen Sie das Anwendungsmodul
start
aus. - Klicken Sie auf die Schaltfläche
Ausführen und wählen Sie entweder einen Emulator aus oder verbinden Sie Ihr Android-Gerät. Auf diesem muss Android Lollipop ausgeführt werden können (die unterstützte Mindest-SDK-Version ist 21). Der Bildschirm „Kotlin Coroutines“ sollte angezeigt werden:
In dieser Starter-App werden Threads verwendet, um den Zähler mit einer kurzen Verzögerung zu erhöhen, nachdem Sie auf das Display getippt haben. Außerdem wird ein neuer Titel aus dem Netzwerk abgerufen und auf dem Bildschirm angezeigt. Probieren Sie es jetzt aus. Die Anzahl und die Meldung sollten sich nach einer kurzen Verzögerung ändern. In diesem Codelab wandeln Sie diese Anwendung so um, dass sie Coroutinen verwendet.
In dieser App werden Architekturkomponenten verwendet, 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 Benutzeroberfläche an, registriert Klick-Listener und kann einSnackbar
anzeigen. Es übergibt Ereignisse anMainViewModel
und aktualisiert den Bildschirm basierend aufLiveData
inMainViewModel
.MainViewModel
verarbeitet Ereignisse inonMainViewClicked
und kommuniziert mitMainActivity
überLiveData.
.Executors
definiertBACKGROUND,
, mit dem Vorgänge in einem Hintergrundthread ausgeführt werden können.TitleRepository
ruft Ergebnisse aus dem Netzwerk ab und speichert sie in der Datenbank.
Einem Projekt Coroutinen hinzufügen
Wenn Sie Coroutinen in Kotlin verwenden möchten, müssen Sie die coroutines-core
-Bibliothek in die build.gradle (Module: app)
-Datei Ihres Projekts einfügen. In den Codelab-Projekten wurde dies bereits für Sie erledigt. Sie müssen es also nicht tun, um das Codelab abzuschließen.
Coroutines für Android sind als Core-Bibliothek und als Android-spezifische Erweiterungen verfügbar:
- kotlinx-coroutines-core : Hauptschnittstelle für die Verwendung von Coroutinen in Kotlin
- kotlinx-coroutines-android : Unterstützung für den Android-Hauptthread in Ko-Routinen
Die Starter-App enthält die Abhängigkeiten bereits in build.gradle.
Wenn Sie ein neues App-Projekt erstellen, müssen Sie build.gradle (Module: app)
öffnen und dem Projekt die Coroutinen-Abhängigkeiten hinzufügen.
dependencies { ... implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:x.x.x" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:x.x.x" }
Unter Android ist es wichtig, den Hauptthread nicht zu blockieren. Der Hauptthread ist ein einzelner Thread, der alle Aktualisierungen der Benutzeroberfläche verarbeitet. Über diesen Thread werden auch alle Click-Handler und andere UI-Callbacks aufgerufen. Daher muss sie reibungslos ablaufen, um eine gute Nutzererfahrung zu gewährleisten.
Damit Ihre App dem Nutzer ohne sichtbare Pausen angezeigt wird, muss der Hauptthread den Bildschirm alle 16 ms oder mehr aktualisieren, was etwa 60 Frames pro Sekunde entspricht. Viele gängige 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 solchen Code über den Hauptthread aufrufen, kann es daher zu Pausen, Rucklern oder sogar zum Einfrieren der App kommen. Wenn Sie den Haupt-Thread zu lange blockieren, kann die App sogar abstürzen und das Dialogfeld Anwendung reagiert nicht wird angezeigt.
Im folgenden Video wird erläutert, wie Coroutinen dieses Problem auf Android lösen, indem sie die Hauptthread-Sicherheit einführen.
Das Callback-Muster
Ein Muster zum Ausführen zeitaufwendiger Aufgaben, ohne den Hauptthread zu blockieren, sind Callbacks. Mit Callbacks können Sie lang andauernde Aufgaben in einem Hintergrundthread starten. Wenn die Aufgabe abgeschlossen ist, wird der Callback aufgerufen, um Sie über das Ergebnis im Hauptthread zu informieren.
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, um im Hauptthread ausgeführt zu werden. Das bedeutet, dass die Funktion sehr schnell zurückgegeben werden muss, damit die nächste Bildschirmaktualisierung nicht verzögert wird. Da slowFetch
jedoch Sekunden oder sogar Minuten dauern kann, kann der Hauptthread nicht auf das Ergebnis warten. Mit dem show(result)
-Callback kann slowFetch
in einem Hintergrundthread ausgeführt und das Ergebnis zurückgegeben werden, sobald es bereit ist.
Callbacks mit Coroutinen entfernen
Callbacks sind ein gutes Muster, haben aber einige Nachteile. Code, in dem viele Callbacks verwendet werden, kann schwer zu lesen und zu verstehen sein. Außerdem können in Callbacks einige Sprachfunktionen wie Ausnahmen nicht verwendet werden.
Mit Kotlin-Coroutinen können Sie Callback-basierten Code in sequenziellen Code umwandeln. Sequenziell geschriebener Code ist in der Regel leichter zu lesen und kann sogar Sprachfunktionen wie Ausnahmen verwenden.
Letztendlich tun sie genau dasselbe: Sie warten, bis ein Ergebnis von einer lang andauernden Aufgabe verfügbar ist, und setzen dann die Ausführung fort. Im Code sehen sie jedoch sehr unterschiedlich aus.
Mit dem Keyword suspend
wird in Kotlin eine Funktion oder ein Funktionstyp markiert, die bzw. der für Coroutinen verfügbar ist. Wenn eine Coroutine eine mit suspend
markierte Funktion aufruft, wird die Ausführung nicht wie bei einem normalen Funktionsaufruf blockiert, bis die Funktion zurückgegeben wird. Stattdessen wird die Ausführung angehalten, bis das Ergebnis verfügbar ist. Anschließend wird die Ausführung mit dem Ergebnis an der Stelle fortgesetzt, an der sie angehalten wurde. Während die Funktion auf ein Ergebnis wartet, wird der Thread, auf dem sie ausgeführt wird, nicht blockiert , sodass andere Funktionen oder Coroutinen ausgeführt werden können.
Im folgenden Code sind makeNetworkRequest()
und slowFetch()
beispielsweise 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 Hauptthread zurückgegeben werden, da sie mit @UiThread
gekennzeichnet ist. Das bedeutet, dass in der Regel keine blockierenden Methoden wie slowFetch
aufgerufen werden konnten. Hier kommt das Keyword suspend
ins Spiel.
Im Vergleich zu Callback-basiertem Code wird mit Coroutine-Code dasselbe Ergebnis erzielt, nämlich das Entsperren des aktuellen Threads, aber mit weniger Code. Aufgrund des sequenziellen Stils lassen sich mehrere lang andauernde Aufgaben einfach verketten, ohne mehrere Rückrufe zu erstellen. Beispielsweise kann Code, der ein Ergebnis von zwei Netzwerkendpunkten abruft und in der Datenbank speichert, als Funktion in Coroutinen ohne Callbacks geschrieben werden. Beispiel:
// 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 Coroutinen in die Beispiel-App ein.
In dieser Übung schreiben Sie eine Coroutine, um eine Nachricht nach einer Verzögerung anzuzeigen. Öffnen Sie zuerst das Modul start
in Android Studio.
CoroutineScope
In Kotlin werden alle Coroutinen in einem CoroutineScope
ausgeführt. Ein Bereich steuert die Lebensdauer von Coroutinen über seinen Job. Wenn Sie den Job eines Bereichs abbrechen, werden alle in diesem Bereich gestarteten Coroutinen abgebrochen. Unter Android können Sie einen Bereich verwenden, um alle laufenden Coroutinen abzubrechen, wenn der Nutzer beispielsweise von einem Activity
oder Fragment
wegnavigiert. Mit Bereichen können Sie auch einen Standard-Dispatcher angeben. Ein Dispatcher steuert, in welchem Thread eine Coroutine ausgeführt wird.
Für von der Benutzeroberfläche gestartete Coroutinen ist es in der Regel richtig, sie auf Dispatchers.Main
zu starten, dem Hauptthread unter Android. Eine auf Dispatchers.Main
gestartete Coroutine blockiert den Hauptthread nicht, während sie angehalten wird. Da eine ViewModel
-Coroutine die Benutzeroberfläche fast immer im Hauptthread aktualisiert, sparen Sie sich durch das Starten von Coroutinen im Hauptthread zusätzliche Threadwechsel. Eine im Hauptthread gestartete Coroutine kann den Dispatcher jederzeit nach dem Start wechseln. So kann beispielsweise ein anderer Dispatcher verwendet werden, um ein großes JSON-Ergebnis außerhalb des Haupt-Threads zu parsen.
viewModelScope verwenden
Die AndroidX lifecycle-viewmodel-ktx
-Bibliothek fügt ViewModels einen CoroutineScope hinzu, der für den Start von UI-bezogenen Coroutinen konfiguriert ist. Wenn Sie diese Bibliothek verwenden möchten, müssen Sie sie in die Datei build.gradle (Module: start)
Ihres Projekts einfügen. Dieser Schritt wurde in den Codelab-Projekten bereits ausgeführt.
dependencies { ... implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:x.x.x" }
Die Bibliothek fügt der Klasse ViewModel
eine viewModelScope
als Erweiterungsfunktion hinzu. Dieser Bereich ist an Dispatchers.Main
gebunden und wird automatisch abgebrochen, wenn ViewModel
gelöscht wird.
Von Threads zu Coroutinen wechseln
Suchen Sie in MainViewModel.kt
nach dem nächsten TODO zusammen 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 BACKGROUND ExecutorService
(definiert in util/Executor.kt
) verwendet, um in einem Hintergrundthread ausgeführt zu werden. Da sleep
den aktuellen Thread blockiert, würde die Benutzeroberfläche einfrieren, wenn die Funktion im Hauptthread aufgerufen würde. Eine Sekunde nach dem Klicken des Nutzers auf die Hauptansicht wird eine Snackbar angefordert.
Sie können das sehen, indem Sie den BACKGROUND-Befehl aus dem Code entfernen und ihn noch einmal ausführen. Der Lade-Spinner wird nicht angezeigt und alles „springt“ eine Sekunde später in den endgültigen Zustand.
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 auf Coroutinen basierenden 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")
}
}
Dieser Code macht dasselbe: Er wartet eine Sekunde, bevor eine Snackbar angezeigt wird. Es gibt jedoch einige wichtige Unterschiede:
viewModelScope.
launch
startet eine Coroutine imviewModelScope
. Wenn der Job, den wir anviewModelScope
übergeben haben, abgebrochen wird, werden alle Coroutinen in diesem Job/Bereich abgebrochen. Wenn der Nutzer die Aktivität vor der Rückgabe vondelay
verlassen hat, wird diese Coroutine automatisch abgebrochen, wennonCleared
beim Beenden des ViewModels aufgerufen wird.- Da
viewModelScope
den Standard-DispatcherDispatchers.Main
hat, wird diese Coroutine im Hauptthread gestartet. Später sehen wir uns an, wie verschiedene Threads verwendet werden. - Die Funktion
delay
ist einesuspend
-Funktion. In Android Studio wird dies durch das Symbolin der linken Spalte angezeigt. Obwohl diese Coroutine im Hauptthread ausgeführt wird, blockiert
delay
den Thread nicht für eine Sekunde. Stattdessen plant der Dispatcher, dass die Coroutine in einer Sekunde bei der nächsten Anweisung fortgesetzt wird.
Führen Sie ihn aus. Wenn Sie auf die Hauptansicht klicken, sollte eine Sekunde später eine Snackbar angezeigt werden.
Im nächsten Abschnitt wird beschrieben, wie Sie diese Funktion testen können.
In dieser Übung schreiben Sie einen Test für den Code, den Sie gerade geschrieben haben. In dieser Übung erfahren Sie, wie Sie mit der Bibliothek kotlinx-coroutines-test auf Dispatchers.Main
ausgeführte Coroutinen testen. Später in diesem Codelab implementieren Sie einen Test, der direkt mit Coroutinen interagiert.
Bestehenden Code prüfen
Öffnen Sie 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")
))
}
}
Eine Regel ist eine Möglichkeit, Code vor und nach der Ausführung eines Tests in JUnit auszuführen. Wir verwenden zwei Regeln, um MainViewModel in einem Off-Device-Test zu testen:
InstantTaskExecutorRule
ist eine JUnit-Regel, dieLiveData
so konfiguriert, dass jede Aufgabe synchron ausgeführt wird.MainCoroutineScopeRule
ist eine benutzerdefinierte Regel in dieser Codebasis, dieDispatchers.Main
so konfiguriert, dass einTestCoroutineDispatcher
auskotlinx-coroutines-test
verwendet wird. Dadurch können Tests eine virtuelle Uhr für Tests vorantreiben und Code kannDispatchers.Main
in Unittests verwenden.
In der Methode setup
wird eine neue Instanz von MainViewModel
mit Test-Fakes erstellt. Das sind Fake-Implementierungen des Netzwerks und der Datenbank, die im Startcode bereitgestellt werden, um Tests ohne Verwendung des echten Netzwerks oder der echten Datenbank zu schreiben.
Für diesen Test sind die Fakes nur erforderlich, um die Abhängigkeiten von MainViewModel
zu erfüllen. Später in diesem Codelab aktualisieren Sie die Fakes, um Coroutinen zu unterstützen.
Test schreiben, der Coroutinen steuert
Fügen Sie einen neuen Test hinzu, der dafür sorgt, dass die Taps eine Sekunde nach dem Klicken auf die Hauptansicht aktualisiert werden:
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 gerade erstellte Coroutine gestartet. Bei diesem Test wird geprüft, ob der Text für die Anzahl der Taps direkt nach dem Aufrufen von onMainViewClicked
„0 Taps“ lautet und eine Sekunde später zu „1 Tap“ aktualisiert wird.
In diesem Test wird virtual-time verwendet, um die Ausführung der von onMainViewClicked
gestarteten Coroutine zu steuern. Mit dem MainCoroutineScopeRule
können Sie die Ausführung von Coroutinen, die auf dem Dispatchers.Main
gestartet werden, pausieren, fortsetzen oder steuern. Hier rufen wir advanceTimeBy(1_000)
auf. Dadurch werden die Coroutinen, die für die Fortsetzung in einer Sekunde geplant sind, sofort vom Haupt-Dispatcher ausgeführt.
Dieser Test ist vollständig deterministisch, d. h., er wird immer auf dieselbe Weise ausgeführt. Da sie die Ausführung von auf Dispatchers.Main
gestarteten Coroutinen vollständig steuern kann, muss sie nicht eine Sekunde warten, bis der Wert festgelegt wird.
Vorhandenen Test ausführen
- Klicken Sie mit der rechten Maustaste auf den Klassennamen
MainViewModelTest
in Ihrem Editor, um ein Kontextmenü zu öffnen. - Wählen Sie im Kontextmenü
Run 'MainViewModelTest' aus.
- Bei zukünftigen Ausführungen können Sie diese Testkonfiguration in den Konfigurationen neben der Schaltfläche
in der Symbolleiste auswählen. Standardmäßig wird die Konfiguration MainViewModelTest genannt.
Der Test sollte bestanden werden. Die Ausführung sollte deutlich weniger als eine Sekunde dauern.
In der nächsten Übung erfahren Sie, wie Sie vorhandene Callback-APIs in Coroutinen umwandeln.
In diesem Schritt beginnen Sie mit der Umstellung eines Repositorys auf die Verwendung von Coroutinen. Dazu fügen wir den ViewModel
, Repository
, Room
und Retrofit
Coroutinen hinzu.
Es ist sinnvoll, die Aufgaben der einzelnen Teile der Architektur zu verstehen, bevor wir sie auf die Verwendung von Coroutinen umstellen.
MainDatabase
implementiert eine Datenbank mit Room, in der einTitle
gespeichert und geladen wird.MainNetwork
implementiert eine Netzwerk-API, mit der ein neuer Titel abgerufen wird. Es wird Retrofit verwendet, um Titel abzurufen.Retrofit
ist so konfiguriert, dass zufällig Fehler oder Mock-Daten zurückgegeben werden. Ansonsten verhält es sich so, als würden echte Netzwerkanfragen gestellt.TitleRepository
implementiert eine einzelne API zum Abrufen oder Aktualisieren des Titels, indem Daten aus dem Netzwerk und der Datenbank kombiniert werden.MainViewModel
stellt den Status des Bildschirms dar und verarbeitet Ereignisse. Dadurch wird das Repository angewiesen, den Titel zu aktualisieren, wenn der Nutzer auf den Bildschirm tippt.
Da die Netzwerkanfrage durch UI-Ereignisse ausgelöst wird und wir auf deren Grundlage eine Coroutine starten möchten, ist der natürliche Ort für den Beginn der Verwendung von Coroutines die ViewModel
.
Die Callback-Version
Öffnen Sie 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. Dadurch wird der Titel im Repository aktualisiert und der neue Titel in die Datenbank geschrieben.
Bei dieser Implementierung wird ein Callback für folgende Aufgaben verwendet:
- Bevor eine Abfrage gestartet wird, wird ein Ladesymbol mit
_spinner.value = true
angezeigt. - Wenn ein Ergebnis zurückgegeben wird, wird der Ladespinner mit
_spinner.value = false
entfernt. - Wenn ein Fehler auftritt, wird eine Snackbar angezeigt und der Spinner wird entfernt.
Der onCompleted
-Callback erhält nicht die title
. Da wir alle Titel in die Room
-Datenbank schreiben, wird die Benutzeroberfläche mit dem aktuellen Titel aktualisiert, indem ein LiveData
beobachtet wird, das von Room
aktualisiert wird.
Bei der Aktualisierung von Coroutines bleibt das Verhalten genau gleich. Es ist ein gutes Muster, eine beobachtbare Datenquelle wie eine Room
-Datenbank zu verwenden, um die Benutzeroberfläche automatisch auf dem neuesten Stand zu halten.
Die Coroutinen-Version
Schreiben wir refreshTitle
mit Koroutinen neu.
Da wir sie gleich benötigen, erstellen wir eine leere suspend-Funktion in unserem Repository (TitleRespository.kt
). Definieren Sie eine neue Funktion, die den suspend
-Operator verwendet, um Kotlin mitzuteilen, dass sie mit Coroutinen funktioniert.
TitleRepository.kt
suspend fun refreshTitle() {
// TODO: Refresh from network and write to database
delay(500)
}
Wenn Sie dieses Codelab abgeschlossen haben, aktualisieren Sie es, um mit Retrofit und Room einen neuen Titel abzurufen und ihn mit Coroutinen in die Datenbank zu schreiben. Vorerst wird es nur 500 Millisekunden lang so tun, als würde es etwas tun, und dann fortfahren.
Ersetzen Sie in MainViewModel
die Callback-Version von refreshTitle
durch eine, die eine neue Coroutine 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
}
}
}
Sehen wir uns diese Funktion einmal genauer an:
viewModelScope.launch {
Genau wie bei der Coroutine zum Aktualisieren der Anzahl der Taps starten Sie zuerst eine neue Coroutine in viewModelScope
. Dabei wird Dispatchers.Main
verwendet, was in Ordnung ist. Auch wenn refreshTitle
eine Netzwerkanfrage und eine Datenbankabfrage ausführt, kann es mit Coroutinen eine Main-safe-Schnittstelle bereitstellen. Das bedeutet, dass es sicher ist, sie aus dem Hauptthread aufzurufen.
Da wir viewModelScope
verwenden, wird die von dieser Coroutine gestartete Arbeit automatisch abgebrochen, wenn der Nutzer diesen Bildschirm verlässt. Das bedeutet, dass keine zusätzlichen Netzwerkanfragen oder Datenbankabfragen erfolgen.
In den nächsten Codezeilen wird refreshTitle
im repository
aufgerufen.
try {
_spinner.value = true
repository.refreshTitle()
}
Bevor diese Coroutine etwas tut, startet sie den Ladespinner und ruft dann refreshTitle
wie eine reguläre Funktion auf. Da refreshTitle
jedoch eine unterbrechbare Funktion ist, wird sie anders als eine normale Funktion ausgeführt.
Wir müssen keinen Callback übergeben. Die Coroutine wird angehalten, bis sie von refreshTitle
fortgesetzt wird. Obwohl es wie ein normaler blockierender Funktionsaufruf aussieht, wird automatisch gewartet, bis die Netzwerk- und Datenbankabfrage abgeschlossen ist, bevor die Ausführung fortgesetzt wird, ohne den Hauptthread zu blockieren.
} catch (error: TitleRefreshError) {
_snackBar.value = error.message
} finally {
_spinner.value = false
}
Ausnahmen in suspend-Funktionen funktionieren genau wie Fehler in regulären Funktionen. Wenn Sie in einer suspend-Funktion einen Fehler auslösen, wird er an den Aufrufer weitergegeben. Obwohl sie sich in der Ausführung stark unterscheiden, können Sie sie mit regulären Try/Catch-Blöcken verarbeiten. Das ist nützlich, weil Sie sich auf die integrierte Sprachunterstützung für die Fehlerbehandlung verlassen können, anstatt für jeden Callback eine benutzerdefinierte Fehlerbehandlung zu erstellen.
Wenn Sie eine Ausnahme aus einer Coroutine auslösen, wird die übergeordnete Coroutine standardmäßig abgebrochen. So lassen sich mehrere zusammengehörige Aufgaben ganz einfach gemeinsam abbrechen.
In einem „finally“-Block können wir dafür sorgen, dass der Spinner nach dem Ausführen der Abfrage immer deaktiviert wird.
Führen Sie die Anwendung noch einmal aus, indem Sie die start-Konfiguration auswählen und danndrücken. Wenn Sie irgendwo tippen, sollte ein Ladesymbol angezeigt werden. Der Titel bleibt gleich, da wir unser Netzwerk oder unsere Datenbank noch nicht verknüpft haben.
In der nächsten Übung aktualisieren Sie das Repository, damit es tatsächlich etwas tut.
In dieser Übung erfahren Sie, wie Sie den Thread wechseln, auf dem eine Coroutine ausgeführt wird, um eine funktionierende Version von TitleRepository
zu implementieren.
Vorhandenen Callback-Code in „refreshTitle“ prüfen
Öffnen Sie TitleRepository.kt
und sehen Sie sich die vorhandene Callback-basierte Implementierung an.
TitleRepository.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 Lade- und Fehlerstatus an den Aufrufer zu übermitteln.
Diese Funktion führt einige Schritte aus, um die Aktualisierung zu implementieren.
- Zu einer anderen Unterhaltung mit
BACKGROUND
ExecutorService
wechseln - Führen Sie die
fetchNextTitle
-Netzwerkanfrage mit der blockierenden Methodeexecute()
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 mit
insertTitle
in der Datenbank und rufen Sie die MethodeonCompleted()
auf. - Wenn das Ergebnis nicht erfolgreich war oder eine Ausnahme vorliegt, rufen Sie die onError-Methode auf, um den Aufrufer über die fehlgeschlagene Aktualisierung zu informieren.
Diese auf Callbacks basierende Implementierung ist main-safe, da sie den Hauptthread nicht blockiert. Es muss jedoch ein Callback verwendet werden, um den Aufrufer zu informieren, wenn die Arbeit abgeschlossen ist. Außerdem werden die Callbacks im Thread BACKGROUND
aufgerufen, zu dem gewechselt wurde.
Blockieren von Anrufen aus Coroutinen
Ohne Coroutinen in das Netzwerk oder die Datenbank einzuführen, können wir diesen Code mit Coroutinen main-safe machen. Dadurch können wir den Callback entfernen und das Ergebnis an den Thread zurückgeben, der ihn ursprünglich aufgerufen hat.
Sie können dieses Muster immer dann verwenden, wenn Sie blockierende oder CPU-intensive Aufgaben aus einer Coroutine heraus ausführen müssen, z. B. das Sortieren und Filtern einer großen Liste oder das Lesen von der Festplatte.
Um zwischen Dispatchern zu wechseln, verwendet Coroutines withContext
. Beim Aufrufen von withContext
wird nur für das Lambda zum anderen Dispatcher gewechselt. Anschließend wird mit dem Ergebnis dieses Lambdas zum aufrufenden Dispatcher zurückgekehrt.
Standardmäßig bietet Kotlin-Coroutines drei Dispatcher: Main
, IO
und Default
. Der IO-Dispatcher ist für E/A-Vorgänge wie das Lesen aus dem Netzwerk oder von der Festplatte optimiert, während der Standard-Dispatcher für CPU-intensive Aufgaben optimiert ist.
TitleRepository.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 blockierende Aufrufe für das Netzwerk und die Datenbank verwendet. Sie ist aber immer noch etwas einfacher als die Callback-Version.
In diesem Code werden weiterhin blockierende Aufrufe verwendet. Durch Aufrufen von execute()
und insertTitle(...)
wird der Thread blockiert, in dem diese Coroutine ausgeführt wird. Wenn wir jedoch mit withContext
zu Dispatchers.IO
wechseln, blockieren wir einen der Threads im IO-Dispatcher. Die Coroutine, die diese Funktion aufgerufen hat und möglicherweise auf Dispatchers.Main
ausgeführt wird, wird angehalten, 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, der es aufgerufen hat, in diesem FallDispatchers.Main
. In der Callback-Version wurden die Callbacks in einem Thread imBACKGROUND
-Executor-Dienst aufgerufen.- Der Aufrufer muss dieser Funktion keinen Callback übergeben. Sie können sich auf das Anhalten 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, auf Coroutinen basierende Implementierung Ergebnisse aus dem Netzwerk lädt.
Im nächsten Schritt integrieren Sie Coroutinen in Room und Retrofit.
Um die Coroutines-Integration fortzusetzen, verwenden wir die Unterstützung für Suspend-Funktionen in der stabilen Version von Room und Retrofit. Anschließend vereinfachen wir den gerade geschriebenen Code erheblich, indem wir die Suspend-Funktionen verwenden.
Coroutines in Room
Öffnen Sie zuerst MainDatabase.kt
und machen Sie insertTitle
zu einer Suspend-Funktion:
MainDatabase.kt
// add the suspend modifier to the existing insertTitle
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertTitle(title: Title)
Wenn Sie das tun, macht Room Ihre Abfrage main-safe und führt sie automatisch in einem Hintergrundthread aus. Das bedeutet aber auch, dass Sie diese Abfrage nur innerhalb einer Coroutine aufrufen können.
Das ist alles, was Sie tun müssen, um Coroutinen in Room zu verwenden. Ziemlich praktisch.
Coroutines in Retrofit
Als Nächstes sehen wir uns an, wie sich Coroutinen in Retrofit einbinden lassen. Öffnen Sie MainNetwork.kt
und ändern Sie fetchNextTitle
in eine Suspend-Funktion.
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 Suspend-Funktionen mit Retrofit verwenden möchten, müssen Sie zwei Dinge tun:
- Der Funktion einen Suspend-Modifikator hinzufügen
- Entfernen Sie den
Call
-Wrapper aus dem Rückgabetyp. Hier wirdString
zurückgegeben. Sie könnten aber auch einen komplexen JSON-basierten Typ zurückgeben. Wenn Sie weiterhin Zugriff auf die vollständigeResult
von Retrofit gewähren möchten, können SieResult<String>
anstelle vonString
aus der Funktion „suspend“ zurückgeben.
Retrofit macht Suspend-Funktionen automatisch main-safe, sodass Sie sie direkt aus Dispatchers.Main
aufrufen können.
Room und Retrofit verwenden
Da Room und Retrofit jetzt Suspend-Funktionen unterstützen, können wir sie in unserem Repository verwenden. Öffnen Sie TitleRepository.kt
und sehen Sie sich an, wie die Verwendung von suspend-Funktionen die Logik erheblich vereinfacht, selbst im Vergleich zur blockierenden Version:
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 viel kürzer. Was ist passiert? Es stellt sich heraus, dass der Code durch die Verwendung von „suspend“ und „resume“ viel kürzer wird. Mit Retrofit können wir hier Rückgabetypen wie String
oder ein User
-Objekt anstelle von Call
verwenden. Das ist sicher, da Retrofit
in der suspend-Funktion die Netzwerkanfrage in einem Hintergrundthread ausführen und die Coroutine fortsetzen kann, wenn der Aufruf abgeschlossen ist.
Noch besser: Wir haben die withContext
entfernt. Da sowohl Room als auch Retrofit main-safe-Funktionen zum Anhalten bereitstellen, ist es sicher, diese asynchrone Arbeit über Dispatchers.Main
zu orchestrieren.
Compilerfehler beheben
Die Umstellung auf Coroutinen erfordert eine Änderung der Signatur von Funktionen, da eine suspend-Funktion nicht aus einer regulären Funktion aufgerufen werden kann. Als Sie in diesem Schritt den Modifikator suspend
hinzugefügt haben, wurden einige Compilerfehler generiert, die zeigen, was passieren würde, wenn Sie in einem echten Projekt eine Funktion in eine suspend-Funktion ändern.
Gehen Sie das Projekt durch und beheben Sie die Compilerfehler, indem Sie die Funktion in eine suspend-Funktion ändern. Hier finden Sie die schnellen Lösungen für die einzelnen Fälle:
TestingFakes.kt
Aktualisieren Sie die Testfakes, um die neuen Suspend-Modifizierer zu unterstützen.
TitleDaoFake
- Drücken Sie Alt+Eingabetaste, um allen Funktionen in der Hierarchie Unterbrechungsmodifikatoren hinzuzufügen.
MainNetworkFake
- Drücken Sie Alt+Eingabetaste, um allen Funktionen in der Hierarchie Unterbrechungsmodifikatoren hinzuzufügen.
fetchNextTitle
durch diese Funktion ersetzen
override suspend fun fetchNextTitle() = result
MainNetworkCompletableFake
- Drücken Sie Alt+Eingabetaste, um allen Funktionen in der Hierarchie Unterbrechungsmodifikatoren hinzuzufügen.
fetchNextTitle
durch diese Funktion ersetzen
override suspend fun fetchNextTitle() = completable.await()
TitleRepository.kt
- Löschen Sie die Funktion
refreshTitleWithCallbacks
, da sie nicht mehr verwendet wird.
App ausführen
Führen Sie die App noch einmal aus. Sobald sie kompiliert ist, sehen Sie, dass Daten mithilfe von Coroutinen vom ViewModel bis zu Room und Retrofit geladen werden.
Herzlichen Glückwunsch! Sie haben die App vollständig auf die Verwendung von Coroutinen umgestellt. Zum Schluss sprechen wir noch darüber, wie Sie das, was wir gerade gemacht haben, testen können.
In dieser Übung schreiben Sie einen Test, der eine suspend
-Funktion direkt aufruft.
Da refreshTitle
als öffentliche API verfügbar ist, wird sie direkt getestet. Dabei wird gezeigt, wie Coroutinenfunktionen aus Tests aufgerufen werden.
Hier ist die Funktion refreshTitle
, die Sie in der letzten Übung implementiert haben:
TitleRepository.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 Suspend-Funktion aufruft
Öffnen Sie TitleRepositoryTest.kt
im Ordner test
, der zwei TO DOs enthält.
Versuchen Sie, refreshTitle
über den ersten Test whenRefreshTitleSuccess_insertsRows
anzurufen.
@Test
fun whenRefreshTitleSuccess_insertsRows() {
val subject = TitleRepository(
MainNetworkFake("OK"),
TitleDaoFake("title")
)
subject.refreshTitle()
}
Da refreshTitle
eine suspend
-Funktion ist, weiß Kotlin nicht, wie sie aufgerufen werden soll, außer über eine Coroutine oder eine andere Suspend-Funktion. Sie erhalten einen Compilerfehler wie „Suspend function refreshTitle should be called only from a coroutine or another suspend function.“
Der Test-Runner weiß nichts über Coroutinen, daher können wir diesen Test nicht als „suspend“-Funktion deklarieren. Wir könnten launch
eine Coroutine mit einem CoroutineScope
wie in einem ViewModel
, aber Tests müssen Coroutinen bis zum Abschluss ausführen, bevor sie zurückkehren. Sobald eine Testfunktion zurückgegeben wird, ist der Test beendet. Mit launch
gestartete Coroutinen sind asynchroner Code, der irgendwann in der Zukunft abgeschlossen werden kann. Um asynchronen Code zu testen, müssen Sie dem Test mitteilen, dass er warten soll, bis die Coroutine abgeschlossen ist. Da launch
ein nicht blockierender Aufruf ist, wird er sofort zurückgegeben und kann nach der Rückgabe der Funktion eine Coroutine 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. Behauptungen wie die, dass die Datenbank aktualisiert wurde, wären unzuverlässig. Wenn refreshTitle
eine Ausnahme ausgelöst hat, wird sie nicht im Testaufrufstapel ausgelöst. Stattdessen wird sie in den nicht abgefangenen Ausnahme-Handler von GlobalScope
eingefügt.
Die Bibliothek kotlinx-coroutines-test
enthält die Funktion runBlockingTest
, die blockiert wird, während sie suspend-Funktionen aufruft. Wenn runBlockingTest
eine suspend-Funktion oder launches
eine neue Coroutine aufruft, wird sie standardmäßig sofort ausgeführt. Sie können sich das als Möglichkeit vorstellen, suspend-Funktionen und Coroutinen in normale Funktionsaufrufe zu konvertieren.
Außerdem löst runBlockingTest
nicht abgefangene Ausnahmen für Sie neu aus. Dadurch wird das Testen erleichtert, wenn eine Coroutine eine Ausnahme auslöst.
Test mit einer Coroutine implementieren
Umschließen Sie den Aufruf von refreshTitle
mit runBlockingTest
und entfernen Sie den GlobalScope.launch
-Wrapper aus subject.refreshTitle().
TitleRepositoryTest.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 diesem Test werden die bereitgestellten Fakes verwendet, um zu prüfen, ob „OK“ von refreshTitle
in die Datenbank eingefügt wird.
Wenn im Test runBlockingTest
aufgerufen wird, wird der Test blockiert, bis die von runBlockingTest
gestartete Coroutine abgeschlossen ist. Wenn wir dann refreshTitle
aufrufen, wird der reguläre Mechanismus zum Anhalten und Fortsetzen verwendet, um darauf zu warten, dass die Datenbankzeile zu unserem Fake hinzugefügt wird.
Nach Abschluss der Test-Coroutine wird runBlockingTest
zurückgegeben.
Timeout-Test schreiben
Wir möchten dem Netzwerkanfrage einen kurzen Timeout hinzufügen. Schreiben wir zuerst den Test und implementieren dann das Zeitlimit. So erstellen Sie einen neuen Test:
TitleRepositoryTest.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 das bereitgestellte gefälschte MainNetworkCompletableFake
verwendet. Dabei handelt es sich um ein Netzwerk-Fake, das Anrufer so lange pausiert, bis der Test fortgesetzt wird. Wenn refreshTitle
versucht, eine Netzwerkanfrage zu stellen, bleibt sie für immer hängen, weil wir Zeitüberschreitungen testen möchten.
Anschließend wird eine separate Coroutine gestartet, um refreshTitle
aufzurufen. Dies ist ein wichtiger Teil des Testens von Zeitüberschreitungen. Die Zeitüberschreitung sollte in einer anderen Coroutine als der von runBlockingTest
erstellten erfolgen. Dadurch können wir die nächste Zeile advanceTimeBy(5_000)
aufrufen, wodurch die Zeit um 5 Sekunden voranschreitet und für die andere Coroutine ein Zeitüberschreitungsfehler auftritt.
Dies ist ein vollständiger Timeout-Test, der bestanden wird, sobald wir das Timeout implementieren.
Führen Sie es jetzt aus und sehen Sie sich an, was passiert:
Caused by: kotlinx.coroutines.test.UncompletedCoroutinesError: Test finished with active jobs: ["...]
Eine der Funktionen von runBlockingTest
ist, dass keine Coroutinen nach Abschluss des Tests mehr ausgeführt werden. Wenn am Ende des Tests noch nicht abgeschlossene Coroutinen vorhanden sind, z. B. unsere Launch-Coroutine, schlägt der Test fehl.
Timeout hinzufügen
Öffne TitleRepository
und füge dem Netzwerkabruf ein Zeitlimit von fünf Sekunden hinzu. Dazu können Sie die Funktion withTimeout
verwenden:
TitleRepository.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)
}
}
Führen Sie den Test aus. Wenn Sie die Tests ausführen, sollten alle erfolgreich sein.
In der nächsten Übung erfahren Sie, wie Sie mithilfe von Coroutinen Funktionen höherer Ordnung schreiben.
In dieser Übung refaktorieren Sie refreshTitle
in MainViewModel
, um eine allgemeine Funktion zum Laden von Daten zu verwenden. Hier erfahren Sie, wie Sie Funktionen höherer Ordnung erstellen, die Coroutinen verwenden.
Die aktuelle Implementierung von refreshTitle
funktioniert, aber wir können eine allgemeine Coroutine zum Laden von Daten erstellen, die immer den Spinner anzeigt. Das kann in einer Codebasis hilfreich sein, in der Daten als Reaktion auf mehrere Ereignisse geladen werden und der Ladespinner immer angezeigt werden soll.
Bei der Überprüfung der aktuellen Implementierung ist jede Zeile außer repository.refreshTitle()
Boilerplate-Code, um den Spinner anzuzeigen und Fehler auszugeben.
// 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
}
}
}
Coroutinen in Funktionen höherer Ordnung verwenden
Fügen Sie diesen Code in „MainViewModel.kt“ ein.
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 nun refreshTitle()
, um diese Funktion höherer Ordnung zu verwenden.
MainViewModel.kt
fun refreshTitle() {
launchDataLoad {
repository.refreshTitle()
}
}
Durch die Abstraktion der Logik für das Anzeigen eines Ladesymbols und von Fehlern haben wir den tatsächlichen Code zum Laden von Daten vereinfacht. Das Anzeigen eines Spinners oder einer Fehlermeldung lässt sich leicht auf das Laden beliebiger Daten verallgemeinern. Die tatsächliche Datenquelle und ‑senke müssen jedoch jedes Mal angegeben werden.
Um diese Abstraktion zu erstellen, verwendet launchDataLoad
ein Argument block
, das ein suspend-Lambda ist. Mit einem Suspend-Lambda können Sie Suspend-Funktionen aufrufen. So implementiert Kotlin die Coroutine-Builder launch
und runBlocking
, die wir in diesem Codelab verwendet haben.
// suspend lambda
block: suspend () -> Unit
Um eine suspend-Lambda-Funktion zu erstellen, beginnen Sie mit dem Keyword suspend
. Der Funktionspfeil und der Rückgabetyp Unit
vervollständigen die Deklaration.
Sie müssen nicht oft eigene suspend-Lambdas deklarieren, aber sie können hilfreich sein, um Abstraktionen wie diese zu erstellen, die wiederholte Logik kapseln.
In dieser Übung erfahren Sie, wie Sie auf Coroutinen basierenden Code von WorkManager aus verwenden.
Was ist WorkManager?
Unter Android gibt es viele Optionen für aufschiebbare Hintergrundaufgaben. In dieser Übung erfahren Sie, wie Sie WorkManager in Coroutinen einbinden. WorkManager ist eine kompatible, flexible und einfache Bibliothek für aufschiebbare Hintergrundaufgaben. WorkManager ist die empfohlene Lösung für diese Anwendungsfälle unter Android.
WorkManager ist Teil von Android Jetpack und eine Architekturkomponente für Hintergrundaufgaben, die eine Kombination aus opportunistischer und garantierter Ausführung erfordern. Die opportunistische Ausführung bedeutet, dass WorkManager Ihre Hintergrundaufgaben so bald wie möglich ausführt. Die garantierte Ausführung bedeutet, dass WorkManager sich um die Logik kümmert, um Ihre Arbeit in verschiedenen Situationen zu starten, auch wenn Sie die App verlassen.
Daher ist WorkManager eine gute Wahl für Aufgaben, die irgendwann abgeschlossen werden müssen.
Hier einige Beispiele für Aufgaben, für die sich WorkManager gut eignet:
- Logs hochladen
- Filter auf Bilder anwenden und Bilder speichern
- Lokale Daten regelmäßig mit dem Netzwerk synchronisieren
Koroutinen mit WorkManager verwenden
WorkManager bietet verschiedene Implementierungen der Basisklasse ListanableWorker
für unterschiedliche Anwendungsfälle.
Mit der einfachsten Worker-Klasse können wir eine synchrone Operation von WorkManager ausführen lassen. Nachdem wir unseren Code jedoch bereits so weit konvertiert haben, dass er Coroutinen und suspend-Funktionen verwendet, ist die beste Möglichkeit, WorkManager zu verwenden, die CoroutineWorker
-Klasse, mit der wir unsere doWork()
-Funktion als suspend-Funktion definieren können.
Öffnen Sie RefreshMainDataWork
, um zu beginnen. Sie erweitert bereits CoroutineWorker
und Sie müssen doWork
implementieren.
Rufen Sie in der Funktion suspend
doWork
refreshTitle()
aus dem Repository auf und geben Sie das entsprechende Ergebnis zurück.
Nachdem Sie die TODO-Aufgabe erledigt 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 suspend-Funktion. Im Gegensatz zur einfacheren Worker
-Klasse wird dieser Code NICHT auf dem in Ihrer WorkManager-Konfiguration angegebenen Executor ausgeführt, sondern verwendet stattdessen den Dispatcher im coroutineContext
-Member (standardmäßig Dispatchers.Default
).
CoroutineWorker testen
Keine Codebasis sollte ohne Tests auskommen.
WorkManager bietet verschiedene Möglichkeiten zum Testen Ihrer Worker
-Klassen. Weitere Informationen zur ursprünglichen Testinfrastruktur finden Sie in der Dokumentation.
WorkManager v2.1 führt eine neue Reihe von APIs ein, die das Testen von ListenableWorker
-Klassen und damit auch von CoroutineWorker vereinfachen. In unserem Code verwenden wir eine dieser neuen APIs: TestListenableWorkerBuilder
.
Um den neuen Test hinzuzufügen, aktualisieren Sie 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 die Factory, damit wir das gefälschte Netzwerk einfügen können.
Im Test selbst wird die TestListenableWorkerBuilder
verwendet, um den Worker zu erstellen, den wir dann mit der Methode startWork()
ausführen können.
WorkManager ist nur ein Beispiel dafür, wie mit Coroutinen das API-Design vereinfacht werden kann.
In diesem Codelab haben wir die Grundlagen behandelt, die Sie benötigen, um mit der Verwendung von Coroutinen in Ihrer App zu beginnen.
Wir haben folgende Themen behandelt:
- Wie Sie Coroutinen in Android-Apps einbinden können, sowohl in der Benutzeroberfläche als auch in WorkManager-Jobs, um die asynchrone Programmierung zu vereinfachen.
- So verwenden Sie Coroutinen in einem
ViewModel
, um Daten aus dem Netzwerk abzurufen und in einer Datenbank zu speichern, ohne den Hauptthread zu blockieren. - Außerdem erfahren Sie, wie Sie alle Coroutinen abbrechen, wenn
ViewModel
abgeschlossen ist.
Beim Testen von Coroutine-basiertem Code haben wir beides abgedeckt, indem wir das Verhalten getestet und suspend
-Funktionen direkt aus Tests aufgerufen haben.
Weitere Informationen
Im Codelab Erweiterte Coroutinen mit Flow von Kotlin und LiveData erfährst du mehr über die erweiterte Verwendung von Coroutinen in Android.
Kotlin-Coroutinen haben viele Funktionen, die in diesem Codelab nicht behandelt wurden. Wenn Sie mehr über Kotlin-Coroutinen erfahren möchten, lesen Sie die von JetBrains veröffentlichten Coroutinen-Anleitungen. Weitere Anwendungsbeispiele für Coroutines auf Android finden Sie im Artikel App-Leistung mit Kotlin-Coroutines verbessern.