Kotlin-Koroutinen in Ihrer Android-App verwenden

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 und Room 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 und runBlocking, 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:

Zip herunterladen

... 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:

  1. Wenn Sie die ZIP-Datei kotlin-coroutines heruntergeladen haben, entpacken Sie die Datei.
  2. Öffne das Projekt coroutines-codelab in Android Studio.
  3. Wähle das Anwendungsmodul start aus.
  4. Klicken Sie auf die Schaltfläche Ausführen.pngAusfü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.

  1. MainActivity zeigt die UI an, registriert Klick-Listener und kann eine Snackbar anzeigen. Er übergibt Ereignisse an MainViewModel und aktualisiert den Bildschirm basierend auf LiveData in MainViewModel.
  2. MainViewModel verarbeitet die Ereignisse in onMainViewClicked und kommuniziert mit MainActivity über LiveData.
  3. Mit Executors wird ein BACKGROUND, definiert, der Elemente in einem Hintergrundthread ausführen kann.
  4. 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:

  1. viewModelScope.launch beginnt in der viewModelScope eine Koroutine. Wenn der Job, der an viewModelScope weitergegeben wurde, abgebrochen wird, werden alle Koroutinen in diesem Job bzw. Bereich gelöscht. Wenn der Nutzer die Aktivität verlassen hat, bevor delay zurückgegeben wurde, wird diese Koroutine automatisch abgebrochen, wenn onCleared nach dem Löschen des ViewModel aufgerufen wird.
  2. 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.
  3. Die Funktion delay ist eine suspend-Funktion. Dies wird in Android Studio durch das Symbol links 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:

  1. InstantTaskExecutorRule ist eine JUnit-Regel, mit der LiveData so konfiguriert wird, dass jede Aufgabe synchron ausgeführt wird
  2. MainCoroutineScopeRule ist eine benutzerdefinierte Regel in dieser Codebasis, die Dispatchers.Main konfiguriert, um eine TestCoroutineDispatcher von kotlinx-coroutines-test zu verwenden. Dadurch kann eine virtuelle Uhr für Tests verwendet werden und Code kann in Einheitentests Dispatchers.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

  1. Klicken Sie in Ihrem Editor mit der rechten Maustaste auf den Kursnamen MainViewModelTest, um ein Kontextmenü zu öffnen.
  2. Wählen Sie im Kontextmenü Ausführen.pngAusführen 'MainViewModelTest' aus.
  3. Für zukünftige Ausführungen können Sie diese Testkonfiguration in den Konfigurationen neben der Schaltfläche Ausführen.png 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.

  1. MainDatabase implementiert eine Datenbank mit Room, die eine Title speichert und lädt.
  2. 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.
  3. TitleRepository implementiert eine einzelne API zum Abrufen oder Aktualisieren des Titels. Dazu werden Daten aus dem Netzwerk und der Datenbank kombiniert.
  4. 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 Ausführen.png. 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.

  1. Zu einer anderen Unterhaltung mit BACKGROUND ExecutorService wechseln
  2. Führe die fetchNextTitle-Netzwerkanfrage mit der blockierenden execute()-Methode aus. Dadurch wird die Netzwerkanfrage im aktuellen Thread ausgeführt, in diesem Fall in einem der Threads in BACKGROUND.
  3. Wenn das Ergebnis erfolgreich ist, speichern Sie es in der Datenbank mit insertTitle und rufen Sie die Methode onCompleted() auf.
  4. 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:

  1. withContext gibt das Ergebnis an den Dispatcher zurück, das ihn aufgerufen hat, in diesem Fall Dispatchers.Main. Die Callback-Version hat die Callbacks in einem Thread im BACKGROUND-Executor-Dienst aufgerufen.
  2. 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:

  1. Funktion „Sperrmodus hinzufügen“ hinzufügen
  2. Entfernen Sie den Call-Wrapper aus dem Rückgabetyp. Hier wird String zurückgegeben, aber Sie können auch komplexe JSON-gestützte Typen zurückgeben. Wenn du weiterhin Zugriff auf das vollständige Result einrichten möchtest, kannst du Result<String> statt String ü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

  1. Drücken Sie die ALT-Eingabetaste zum Sperren von Modifikatoren an alle Funktionen in der Hierarchie

MainNetworkfake

  1. Drücken Sie die ALT-Eingabetaste zum Sperren von Modifikatoren an alle Funktionen in der Hierarchie
  2. fetchNextTitle“ durch diese Funktion ersetzen
override suspend fun fetchNextTitle() = result

MainNetworkCompletableFake

  1. Drücken Sie die ALT-Eingabetaste zum Sperren von Modifikatoren an alle Funktionen in der Hierarchie
  2. 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.