Grundlagen von Android und Kotlin 06.2: Coroutinen und Room

Dieses Codelab ist Teil des Kurses „Grundlagen von Android und Kotlin“. Sie können diesen Kurs am besten nutzen, wenn Sie die Codelabs der Reihe nach durcharbeiten. Alle Codelabs des Kurses sind auf der Landingpage für Codelabs zu den Grundlagen von Android und Kotlin aufgeführt.

Einführung

Eine der wichtigsten Prioritäten bei der Entwicklung einer nutzerfreundlichen App ist es, dafür zu sorgen, dass die Benutzeroberfläche immer reagiert und reibungslos funktioniert. Eine Möglichkeit, die Leistung der Benutzeroberfläche zu verbessern, besteht darin, zeitaufwendige Aufgaben wie Datenbankvorgänge in den Hintergrund zu verlagern.

In diesem Codelab implementierst du den nutzerorientierten Teil der App „TrackMySleepQuality“ und verwendest Kotlin-Coroutinen, um Datenbankvorgänge außerhalb des Hauptthreads auszuführen.

Was Sie bereits wissen sollten

Sie sollten mit Folgendem vertraut sein:

  • Erstellen einer einfachen Benutzeroberfläche (UI) mit einer Aktivität, Fragmenten, Ansichten und Click-Handlern.
  • Zwischen Fragmenten navigieren und safeArgs verwenden, um einfache Daten zwischen Fragmenten zu übergeben.
  • Modelle, Modell-Factories, Transformationen und LiveData ansehen
  • So erstellen Sie eine Room-Datenbank, ein DAO und definieren Entitäten.
  • Es ist hilfreich, wenn Sie mit den Konzepten von Threads und Multiprocessing vertraut sind.

Lerninhalte

  • Funktionsweise von Threads in Android
  • So verwenden Sie Kotlin-Coroutinen, um Datenbankvorgänge vom Hauptthread zu trennen.
  • So werden formatierte Daten in einem TextView angezeigt.

Aufgaben

  • Erweitern Sie die App „TrackMySleepQuality“, um Daten in der Datenbank zu erfassen, zu speichern und anzuzeigen.
  • Verwenden Sie Coroutinen, um lange laufende Datenbankvorgänge im Hintergrund auszuführen.
  • Verwenden Sie LiveData, um die Navigation und die Anzeige einer Snackbar auszulösen.
  • Mit LiveData können Sie Schaltflächen aktivieren und deaktivieren.

In diesem Codelab erstellen Sie das View-Modell, die Coroutinen und die Teile der TrackMySleepQuality-App, die für die Datenanzeige zuständig sind.

Die App hat zwei Bildschirme, die durch Fragmente dargestellt werden, wie in der Abbildung unten zu sehen ist.

Auf dem ersten Bildschirm links befinden sich Schaltflächen zum Starten und Beenden des Trackings. Auf dem Bildschirm werden alle Schlafdaten des Nutzers angezeigt. Mit der Schaltfläche Löschen werden alle Daten, die die App für den Nutzer erhoben hat, dauerhaft gelöscht.

Auf dem zweiten Bildschirm rechts können Sie eine Bewertung der Schlafqualität auswählen. In der App wird die Bewertung numerisch dargestellt. Zu Entwicklungszwecken werden in der App sowohl die Gesichtssymbole als auch ihre numerischen Entsprechungen angezeigt.

Der Ablauf für den Nutzer sieht so aus:

  • Der Nutzer öffnet die App und sieht den Bildschirm für die Schlafanalyse.
  • Der Nutzer tippt auf die Schaltfläche Starten. Die Startzeit wird aufgezeichnet und angezeigt. Die Schaltfläche Starten ist deaktiviert und die Schaltfläche Beenden ist aktiviert.
  • Der Nutzer tippt auf die Schaltfläche Stopp. Dadurch wird die Endzeit aufgezeichnet und der Bildschirm „Schlafqualität“ geöffnet.
  • Der Nutzer wählt ein Symbol für die Schlafqualität aus. Der Bildschirm wird geschlossen und auf dem Tracking-Bildschirm werden die Endzeit des Schlafs und die Schlafqualität angezeigt. Die Schaltfläche Beenden ist deaktiviert und die Schaltfläche Starten ist aktiviert. Die App ist bereit für eine weitere Nacht.
  • Die Schaltfläche Löschen ist immer dann aktiviert, wenn Daten in der Datenbank vorhanden sind. Wenn der Nutzer auf die Schaltfläche Löschen tippt, werden alle seine Daten unwiderruflich gelöscht. Es wird keine Bestätigungsmeldung angezeigt.

Diese App verwendet eine vereinfachte Architektur, wie unten im Kontext der vollständigen Architektur dargestellt. Die App verwendet nur die folgenden Komponenten:

  • UI-Controller
  • Modell und LiveData ansehen
  • Eine Room-Datenbank

In dieser Aufgabe verwenden Sie ein TextView, um formatierte Daten zum Schlaftracking anzuzeigen. Dies ist nicht die endgültige Benutzeroberfläche. In einem anderen Codelab erfahren Sie, wie Sie das besser machen können.)

Sie können mit der App „TrackMySleepQuality“ fortfahren, die Sie im vorherigen Codelab erstellt haben, oder die Starter-App für dieses Codelab herunterladen.

Schritt 1: Start-App herunterladen und ausführen

  1. Laden Sie die App TrackMySleepQuality-Coroutines-Starter von GitHub herunter.
  2. Erstellen Sie die App und führen Sie sie aus. In der App wird die Benutzeroberfläche für das Fragment SleepTrackerFragment angezeigt, aber keine Daten. Die Tasten reagieren nicht auf Antippen.

Schritt 2: Code prüfen

Der Startcode für dieses Codelab ist derselbe wie der Lösungscode für das Codelab 6.1 „Room-Datenbank erstellen“.

  1. Öffnen Sie res/layout/activity_main.xml. Dieses Layout enthält das Fragment nav_host_fragment. Beachten Sie auch das <merge>-Tag.

    Mit dem merge-Tag können Sie beim Einbinden von Layouts redundante Layouts vermeiden. Es empfiehlt sich, es zu verwenden. Ein Beispiel für ein redundantes Layout wäre ConstraintLayout > LinearLayout > TextView, wobei das System möglicherweise das LinearLayout eliminieren kann. Diese Art der Optimierung kann die Ansichtshierarchie vereinfachen und die App-Leistung verbessern.
  2. Öffnen Sie im Ordner navigation die Datei navigation.xml. Sie sehen zwei Fragmente und die Navigationsaktionen, die sie verbinden.
  3. Doppelklicken Sie im Ordner layout auf das Fragment für den Schlaftracker, um das XML-Layout aufzurufen. Beachten Sie Folgendes:
  • Die Layoutdaten sind in einem <layout>-Element eingeschlossen, um die Datenbindung zu ermöglichen.
  • ConstraintLayout und die anderen Ansichten sind im Element <layout> angeordnet.
  • Die Datei enthält ein <data>-Platzhalter-Tag.

Die Starter-App enthält auch Dimensionen, Farben und Formatierungen für die Benutzeroberfläche. Die App enthält eine Room-Datenbank, ein DAO und eine SleepNight-Entität. Wenn Sie das vorherige Codelab nicht durchgearbeitet haben, sollten Sie sich diese Aspekte des Codes selbst ansehen.

Nachdem Sie eine Datenbank und eine Benutzeroberfläche haben, müssen Sie Daten erheben, sie der Datenbank hinzufügen und anzeigen. All diese Arbeit wird im Ansichtsmodell erledigt. Das View-Modell für den Schlaftracker verarbeitet Schaltflächenklicks, interagiert über das DAO mit der Datenbank und stellt der Benutzeroberfläche Daten über LiveData zur Verfügung. Alle Datenbankvorgänge müssen außerhalb des Haupt-UI-Threads ausgeführt werden. Dazu verwenden Sie Coroutinen.

Schritt 1: SleepTrackerViewModel hinzufügen

  1. Öffnen Sie im Paket sleeptracker die Datei SleepTrackerViewModel.kt.
  2. Sehen Sie sich die Klasse SleepTrackerViewModel an, die in der Starter-App enthalten ist und auch unten angezeigt wird. Die Klasse erweitert AndroidViewModel(). Diese Klasse entspricht ViewModel, verwendet jedoch den Anwendungskontext als Parameter und macht ihn als Property verfügbar. Sie benötigen ihn später.
class SleepTrackerViewModel(
       val database: SleepDatabaseDao,
       application: Application) : AndroidViewModel(application) {
}

Schritt 2: SleepTrackerViewModelFactory hinzufügen

  1. Öffnen Sie im Paket sleeptracker die Datei SleepTrackerViewModelFactory.kt.
  2. Sehen Sie sich den Code für die Factory an, der unten aufgeführt ist:
class SleepTrackerViewModelFactory(
       private val dataSource: SleepDatabaseDao,
       private val application: Application) : ViewModelProvider.Factory {
   @Suppress("unchecked_cast")
   override fun <T : ViewModel?> create(modelClass: Class<T>): T {
       if (modelClass.isAssignableFrom(SleepTrackerViewModel::class.java)) {
           return SleepTrackerViewModel(dataSource, application) as T
       }
       throw IllegalArgumentException("Unknown ViewModel class")
   }
}

Beachten Sie Folgendes:

  • Die bereitgestellte SleepTrackerViewModelFactory verwendet dasselbe Argument wie ViewModel und erweitert ViewModelProvider.Factory.
  • In der Factory wird create() überschrieben. Diese Funktion akzeptiert einen beliebigen Klassentyp als Argument und gibt ein ViewModel zurück.
  • Im Text von create() wird geprüft, ob eine SleepTrackerViewModel-Klasse verfügbar ist. Wenn dies der Fall ist, wird eine Instanz davon zurückgegeben. Andernfalls wird eine Ausnahme ausgegeben.

Schritt 3: SleepTrackerFragment
aktualisieren

  1. Rufen Sie im SleepTrackerFragment eine Referenz zum Anwendungskontext ab. Fügen Sie die Referenz in onCreateView() unter binding ein. Sie benötigen einen Verweis auf die App, an die dieses Fragment angehängt ist, um ihn an den Anbieter der ViewModel-Factory zu übergeben.

    Die Kotlin-Funktion requireNotNull löst eine IllegalArgumentException aus, wenn der Wert null ist.
val application = requireNotNull(this.activity).application
  1. Sie benötigen einen Verweis auf Ihre Datenquelle über einen Verweis auf das DAO. Definieren Sie in onCreateView() vor dem return ein dataSource. Verwenden Sie SleepDatabase.getInstance(application).sleepDatabaseDao, um einen Verweis auf das DAO der Datenbank abzurufen.
val dataSource = SleepDatabase.getInstance(application).sleepDatabaseDao
  1. Erstellen Sie in onCreateView() vor dem return eine Instanz von viewModelFactory. Sie müssen dataSource und application übergeben.
val viewModelFactory = SleepTrackerViewModelFactory(dataSource, application)
  1. Nachdem Sie eine Factory haben, rufen Sie eine Referenz auf SleepTrackerViewModel ab. Der Parameter SleepTrackerViewModel::class.java bezieht sich auf die Java-Laufzeitklasse dieses Objekts.
val sleepTrackerViewModel =
       ViewModelProviders.of(
               this, viewModelFactory).get(SleepTrackerViewModel::class.java)
  1. Der fertige Code sollte so aussehen:
// Create an instance of the ViewModel Factory.
val dataSource = SleepDatabase.getInstance(application).sleepDatabaseDao
val viewModelFactory = SleepTrackerViewModelFactory(dataSource, application)

// Get a reference to the ViewModel associated with this fragment.
val sleepTrackerViewModel =
       ViewModelProviders.of(
               this, viewModelFactory).get(SleepTrackerViewModel::class.java)

So sieht die onCreateView()-Methode bisher aus:

override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
                              savedInstanceState: Bundle?): View? {

        // Get a reference to the binding object and inflate the fragment views.
        val binding: FragmentSleepTrackerBinding = DataBindingUtil.inflate(
                inflater, R.layout.fragment_sleep_tracker, container, false)

        val application = requireNotNull(this.activity).application

        val dataSource = SleepDatabase.getInstance(application).sleepDatabaseDao

        val viewModelFactory = SleepTrackerViewModelFactory(dataSource, application)

        val sleepTrackerViewModel =
                ViewModelProviders.of(
                        this, viewModelFactory).get(SleepTrackerViewModel::class.java)

        return binding.root
    }

Schritt 4: Datenbindung für das Ansichtsmodell hinzufügen

Nachdem Sie die grundlegende ViewModel eingerichtet haben, müssen Sie die Einrichtung der Datenbindung in der SleepTrackerFragment abschließen, um die ViewModel mit der Benutzeroberfläche zu verbinden.


In der Layoutdatei fragment_sleep_tracker.xml:

  1. Erstellen Sie im Block <data> ein <variable>, das auf die Klasse SleepTrackerViewModel verweist.
<data>
   <variable
       name="sleepTrackerViewModel"
       type="com.example.android.trackmysleepquality.sleeptracker.SleepTrackerViewModel" />
</data>

In SleepTrackerFragment:

  1. Die aktuelle Aktivität als Lebenszyklusinhaber der Bindung festlegen. Fügen Sie diesen Code in die onCreateView()-Methode vor der return-Anweisung ein:
binding.setLifecycleOwner(this)
  1. Weisen Sie die Bindungsvariable sleepTrackerViewModel der sleepTrackerViewModel zu. Fügen Sie diesen Code in onCreateView() ein, unter den Code, mit dem SleepTrackerViewModel erstellt wird:
binding.sleepTrackerViewModel = sleepTrackerViewModel
  1. Wahrscheinlich wird ein Fehler angezeigt, da Sie das Bindungsobjekt neu erstellen müssen. Bereinigen Sie das Projekt und erstellen Sie es neu, um den Fehler zu beheben.
  2. Achten Sie wie immer darauf, dass Ihr Code ohne Fehler erstellt und ausgeführt wird.

In Kotlin sind Coroutinen die Möglichkeit, lang andauernde Aufgaben elegant und effizient zu verarbeiten. 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 Coroutinen und Callbacks dasselbe: Sie warten, bis ein Ergebnis von einer zeitaufwendigen Aufgabe verfügbar ist, und setzen die Ausführung fort.

Coroutinen haben die folgenden Eigenschaften:

  • Coroutinen sind asynchron und nicht blockierend.
  • Bei Coroutinen werden suspend-Funktionen verwendet, um asynchronen Code sequenziell zu machen.

Coroutinen sind asynchron.

Eine Coroutine wird unabhängig von den Hauptausführungsschritten Ihres Programms ausgeführt. Dies kann parallel oder auf einem separaten Prozessor erfolgen. Es könnte auch sein, dass Sie ein wenig Verarbeitung einfügen, während der Rest der App auf Eingaben wartet. Einer der wichtigsten Aspekte von „async“ ist, dass Sie nicht davon ausgehen können, dass das Ergebnis verfügbar ist, bis Sie explizit darauf warten.

Angenommen, Sie haben eine Frage, die Recherche erfordert, und Sie bitten einen Kollegen, die Antwort zu finden. Sie machen sich an die Arbeit, was so viel bedeutet, als würden sie die Arbeit „asynchron“ und „in einem separaten Thread“ erledigen. Sie können mit anderen Aufgaben fortfahren, die nicht von der Antwort abhängen, bis Ihr Kollege zurückkommt und Ihnen die Antwort mitteilt.

Coroutinen sind nicht blockierend.

Nicht blockierend bedeutet, dass eine Coroutine den Haupt- oder UI-Thread nicht blockiert. Mit Coroutinen wird die Benutzeroberfläche immer priorisiert, sodass Nutzer immer die bestmögliche Erfahrung haben.

Bei Coroutinen werden suspend-Funktionen verwendet, um asynchronen Code sequenziell zu machen.

Das Keyword suspend ist die Kotlin-Methode, um eine Funktion oder einen Funktionstyp als für Coroutinen verfügbar zu kennzeichnen. Wenn eine Coroutine eine mit suspend markierte Funktion aufruft, wird die Ausführung der Coroutine nicht wie bei einem normalen Funktionsaufruf blockiert, bis die Funktion zurückkehrt. Stattdessen wird die Ausführung der Coroutine angehalten, bis das Ergebnis verfügbar ist. Die Coroutine wird dann mit dem Ergebnis an der Stelle fortgesetzt, an der sie unterbrochen wurde.

Während die Coroutine angehalten wird und auf ein Ergebnis wartet, wird der Thread, auf dem sie ausgeführt wird, nicht blockiert. So können andere Funktionen oder Coroutinen ausgeführt werden.

Das Keyword suspend gibt nicht den Thread an, in dem der Code ausgeführt wird. Eine Suspend-Funktion kann in einem Hintergrundthread oder im Hauptthread ausgeführt werden.

Um Coroutinen in Kotlin zu verwenden, benötigen Sie drei Dinge:

  • Ein Job
  • Ein Disponent
  • Ein Bereich

Job: Ein Job ist im Grunde alles, was abgebrochen werden kann. Jede Coroutine hat einen Job, mit dem Sie die Coroutine abbrechen können. Jobs können in hierarchischen Beziehungen angeordnet werden. Wenn Sie einen übergeordneten Job abbrechen, werden alle untergeordneten Jobs sofort abgebrochen. Das ist viel praktischer, als jede Coroutine manuell abzubrechen.

Dispatcher : Der Dispatcher sendet Coroutinen zur Ausführung in verschiedenen Threads. Beispielsweise führt Dispatcher.Main Aufgaben im Hauptthread aus und Dispatcher.IO lagert blockierende E/A-Aufgaben in einen gemeinsamen Thread-Pool aus.

Umfang : Der Umfang einer Coroutine definiert den Kontext, in dem die Coroutine ausgeführt wird. Ein Bereich kombiniert Informationen zum Job und Dispatcher einer Coroutine. Mit Scopes werden Coroutinen im Blick behalten. Wenn Sie eine Coroutine starten, befindet sie sich in einem Bereich. Das bedeutet, dass Sie angegeben haben, welcher Bereich die Coroutine im Blick behält.

Der Nutzer soll auf folgende Weise mit den Schlafdaten interagieren können:

  • Wenn der Nutzer auf die Schaltfläche Start tippt, erstellt die App eine neue Schlafnacht und speichert sie in der Datenbank.
  • Wenn der Nutzer auf die Schaltfläche Beenden tippt, aktualisiert die App die Nacht mit einer Endzeit.
  • Wenn der Nutzer auf die Schaltfläche Löschen tippt, löscht die App die Daten in der Datenbank.

Diese Datenbankvorgänge können lange dauern und sollten daher in einem separaten Thread ausgeführt werden.

Schritt 1: Coroutinen für Datenbankvorgänge einrichten

Wenn in der Sleep Tracker App auf die Schaltfläche Start getippt wird, soll eine Funktion in SleepTrackerViewModel aufgerufen werden, um eine neue Instanz von SleepNight zu erstellen und in der Datenbank zu speichern.

Durch Tippen auf eine der Schaltflächen wird ein Datenbankvorgang ausgelöst, z. B. das Erstellen oder Aktualisieren einer SleepNight. Aus diesem und anderen Gründen verwenden Sie Coroutinen, um Klick-Handler für die Schaltflächen der App zu implementieren.

  1. Öffnen Sie die Datei build.gradle auf App-Ebene und suchen Sie nach den Abhängigkeiten für Coroutinen. Wenn Sie Coroutinen verwenden möchten, benötigen Sie diese Abhängigkeiten, die für Sie hinzugefügt wurden.

    Die $coroutine_version ist in der Projektdatei build.gradle als coroutine_version = '1.0.0'definiert.
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutine_version"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutine_version"
  1. Öffnen Sie die Datei SleepTrackerViewModel.
  2. Definieren Sie im Text der Klasse viewModelJob und weisen Sie ihr eine Instanz von Job zu. Mit diesem viewModelJob können Sie alle von diesem ViewModel gestarteten Coroutinen abbrechen, wenn das ViewModel nicht mehr verwendet und zerstört wird. So vermeiden Sie, dass Koroutinen ohne Rückkehrziel entstehen.
private var viewModelJob = Job()
  1. Überschreiben Sie am Ende des Hauptteils der Klasse onCleared() und brechen Sie alle Coroutinen ab. Wenn das ViewModel zerstört wird, wird onCleared() aufgerufen.
override fun onCleared() {
   super.onCleared()
   viewModelJob.cancel()
}
  1. Definieren Sie direkt unter der Definition von viewModelJob einen uiScope für die Coroutinen. Der Bereich bestimmt, in welchem Thread die Coroutine ausgeführt wird, und der Bereich muss auch über den Job informiert sein. Wenn Sie einen Bereich abrufen möchten, fordern Sie eine Instanz von CoroutineScope an und übergeben Sie einen Dispatcher und einen Job.

Wenn Sie Dispatchers.Main verwenden, werden im uiScope gestartete Coroutinen im Hauptthread ausgeführt. Das ist für viele von einem ViewModel gestartete Coroutinen sinnvoll, da sie nach der Verarbeitung zu einer Aktualisierung der Benutzeroberfläche führen.

private val uiScope = CoroutineScope(Dispatchers.Main + viewModelJob)
  1. Definieren Sie unter der Definition von uiScope eine Variable mit dem Namen tonight, die die aktuelle Nacht enthält. Machen Sie die Variable MutableLiveData, da Sie die Daten beobachten und ändern müssen.
private var tonight = MutableLiveData<SleepNight?>()
  1. Um die Variable tonight so schnell wie möglich zu initialisieren, erstellen Sie einen init-Block unter der Definition von tonight und rufen Sie initializeTonight() auf. Sie definieren initializeTonight() im nächsten Schritt.
init {
   initializeTonight()
}
  1. Implementieren Sie unter dem init-Block initializeTonight(). Starten Sie im uiScope eine Koroutine. Rufen Sie den Wert für tonight aus der Datenbank ab, indem Sie getTonightFromDatabase() aufrufen, und weisen Sie den Wert tonight.value zu. Sie definieren getTonightFromDatabase() im nächsten Schritt.
private fun initializeTonight() {
   uiScope.launch {
       tonight.value = getTonightFromDatabase()
   }
}
  1. getTonightFromDatabase() implementieren. Definieren Sie sie als private suspend-Funktion, die ein nullable SleepNight zurückgibt, wenn kein aktuelles SleepNight gestartet wurde. Das führt zu einem Fehler, da die Funktion etwas zurückgeben muss.
private suspend fun getTonightFromDatabase(): SleepNight? { }
  1. Geben Sie im Funktionskörper von getTonightFromDatabase() das Ergebnis einer Coroutine zurück, die im Dispatchers.IO-Kontext ausgeführt wird. Verwende den E/A-Dispatcher, da das Abrufen von Daten aus der Datenbank ein E/A-Vorgang ist und nichts mit der Benutzeroberfläche zu tun hat.
  return withContext(Dispatchers.IO) {}
  1. Lassen Sie die Coroutine im Rückgabeblock die heutige Nacht (die neueste Nacht) aus der Datenbank abrufen. Wenn die Start- und Endzeit nicht identisch sind, was bedeutet, dass die Nacht bereits abgeschlossen ist, gib null zurück. Andernfalls wird die Nacht zurückgegeben.
       var night = database.getTonight()
       if (night?.endTimeMilli != night?.startTimeMilli) {
           night = null
       }
       night

Die fertige getTonightFromDatabase()-Funktion zum Sperren sollte so aussehen. Es sollten keine Fehler mehr auftreten.

private suspend fun getTonightFromDatabase(): SleepNight? {
   return withContext(Dispatchers.IO) {
       var night = database.getTonight()
       if (night?.endTimeMilli != night?.startTimeMilli) {
           night = null
       }
       night
   }
}

Schritt 2: Klick-Handler für die Schaltfläche „Start“ hinzufügen

Jetzt können Sie onStartTracking() implementieren, den Click-Handler für die Schaltfläche Start. Sie müssen ein neues SleepNight erstellen, es in die Datenbank einfügen und tonight zuweisen. Die Struktur von onStartTracking() wird initializeTonight() sehr ähnlich sein.

  1. Beginnen Sie mit der Funktionsdefinition für onStartTracking(). Sie können die Click-Handler über onCleared() in der Datei SleepTrackerViewModel platzieren.
fun onStartTracking() {}
  1. Starten Sie in onStartTracking() eine Coroutine in uiScope, da Sie dieses Ergebnis benötigen, um fortzufahren und die Benutzeroberfläche zu aktualisieren.
uiScope.launch {}
  1. Erstellen Sie innerhalb des Coroutine-Starts ein neues SleepNight, das die aktuelle Zeit als Startzeit erfasst.
        val newNight = SleepNight()
  1. Rufen Sie innerhalb des Coroutine-Starts insert() auf, um newNight in die Datenbank einzufügen. Es wird ein Fehler angezeigt, da Sie die Funktion insert() noch nicht definiert haben. (Dies ist nicht die DAO-Funktion mit demselben Namen.)
       insert(newNight)
  1. Aktualisieren Sie tonight auch innerhalb des Coroutine-Starts.
       tonight.value = getTonightFromDatabase()
  1. Definieren Sie unter onStartTracking() die Funktion insert() als private suspend-Funktion, die ein SleepNight als Argument verwendet.
private suspend fun insert(night: SleepNight) {}
  1. Starte für den Text von insert() eine Coroutine im I/O-Kontext und füge die Nacht in die Datenbank ein, indem du insert() über das DAO aufrufst.
   withContext(Dispatchers.IO) {
       database.insert(night)
   }
  1. Fügen Sie in der Layoutdatei fragment_sleep_tracker.xml den Click-Handler für onStartTracking() in start_button ein. Verwenden Sie dazu die Datenbindung, die Sie zuvor eingerichtet haben. Mit der Funktionsnotation @{() -> wird eine Lambda-Funktion erstellt, die keine Argumente akzeptiert und den Klick-Handler in sleepTrackerViewModel aufruft.
android:onClick="@{() -> sleepTrackerViewModel.onStartTracking()}"
  1. Erstellen Sie Ihre App und führen Sie sie aus. Tippen Sie dazu auf die Schaltfläche Starten. Durch diese Aktion werden Daten erstellt, aber Sie können noch nichts sehen. Das beheben Sie als Nächstes.
fun someWorkNeedsToBeDone {
   uiScope.launch {

        suspendFunction()

   }
}

suspend fun suspendFunction() {
   withContext(Dispatchers.IO) {
       longrunningWork()
   }
}

Schritt 3: Daten darstellen

Im SleepTrackerViewModel verweist die Variable nights auf LiveData, da getAllNights() im DAO LiveData zurückgibt.

Es ist eine Room-Funktion, bei der die LiveData nights jedes Mal aktualisiert wird, wenn sich die Daten in der Datenbank ändern, um die neuesten Daten anzuzeigen. Sie müssen die LiveData nie explizit festlegen oder aktualisieren. Room aktualisiert die Daten entsprechend der Datenbank.

Wenn Sie nights jedoch in einer Textansicht anzeigen, wird die Objektreferenz angezeigt. Wenn Sie den Inhalt des Objekts sehen möchten, müssen Sie die Daten in einen formatierten String umwandeln. Verwenden Sie eine Transformation-Zuordnung, die jedes Mal ausgeführt wird, wenn nights neue Daten aus der Datenbank empfängt.

  1. Öffnen Sie die Datei Util.kt und entfernen Sie die Kommentarzeichen für den Code für die Definition von formatNights() und die zugehörigen import-Anweisungen. Wenn Sie in Android Studio Code auskommentieren möchten, wählen Sie den gesamten mit // gekennzeichneten Code aus und drücken Sie Cmd+/ oder Control+/.
  2. formatNights() gibt den Typ Spanned zurück, einen HTML-formatierten String.
  3. Öffnen Sie strings.xml. Beachten Sie die Verwendung von CDATA zum Formatieren der String-Ressourcen für die Anzeige der Schlafdaten.
  4. Öffne SleepTrackerViewModel. Definieren Sie in der Klasse SleepTrackerViewModel unter der Definition von uiScope eine Variable mit dem Namen nights. Rufen Sie alle Nächte aus der Datenbank ab und weisen Sie sie der Variablen nights zu.
private val nights = database.getAllNights()
  1. Fügen Sie direkt unter der Definition von nights Code hinzu, um nights in eine nightsString umzuwandeln. Verwenden Sie die Funktion formatNights() aus Util.kt.

    . Übergeben Sie nights an die Funktion map() aus der Klasse Transformations. Wenn Sie auf Ihre String-Ressourcen zugreifen möchten, definieren Sie die Zuordnungsfunktion als Aufruf von formatNights(). Geben Sie nights und ein Resources-Objekt an.
val nightsString = Transformations.map(nights) { nights ->
   formatNights(nights, application.resources)
}
  1. Öffnen Sie die Layoutdatei fragment_sleep_tracker.xml. In der TextView können Sie in der Property android:text den Ressourcenstring durch einen Verweis auf nightsString ersetzen.
"@{sleepTrackerViewModel.nightsString}"
  1. Erstelle den Code neu und führe die App aus. Alle deine Schlafdaten mit Startzeiten sollten jetzt angezeigt werden.
  2. Tippe noch einige Male auf die Schaltfläche Starten, um weitere Daten zu sehen.

Im nächsten Schritt aktivieren Sie die Funktion für die Schaltfläche Stop.

Schritt 4: Klick-Handler für die Schaltfläche „Stopp“ hinzufügen

Implementieren Sie den Klick-Handler für die Schaltfläche Stop in SleepTrackerViewModel.. Verwenden Sie dabei dasselbe Muster wie im vorherigen Schritt.

  1. Fügen Sie onStopTracking() zu ViewModel hinzu. Starten Sie eine Koroutine im uiScope. Wenn die Endzeit noch nicht festgelegt wurde, legen Sie endTimeMilli auf die aktuelle Systemzeit fest und rufen Sie update() mit den Daten für die Nacht auf.

    In Kotlin gibt die Syntax return@label die Funktion an, aus der diese Anweisung zurückgegeben wird, wenn mehrere Funktionen verschachtelt sind.
fun onStopTracking() {
   uiScope.launch {
       val oldNight = tonight.value ?: return@launch
       oldNight.endTimeMilli = System.currentTimeMillis()
       update(oldNight)
   }
}
  1. Implementieren Sie update() mit demselben Muster wie für insert().
private suspend fun update(night: SleepNight) {
   withContext(Dispatchers.IO) {
       database.update(night)
   }
}
  1. Um den Click-Handler mit der Benutzeroberfläche zu verbinden, öffnen Sie die Layoutdatei fragment_sleep_tracker.xml und fügen Sie den Click-Handler dem stop_button hinzu.
android:onClick="@{() -> sleepTrackerViewModel.onStopTracking()}"
  1. Erstellen Sie Ihre App und führen Sie sie aus.
  2. Tippe auf Starten und dann auf Beenden. Du siehst die Start- und Endzeit, die Schlafqualität ohne Wert und die geschlafene Zeit.

Schritt 5: Klick-Handler für die Schaltfläche „Löschen“ hinzufügen

  1. Implementieren Sie onClear() und clear() auf ähnliche Weise.
fun onClear() {
   uiScope.launch {
       clear()
       tonight.value = null
   }
}

suspend fun clear() {
   withContext(Dispatchers.IO) {
       database.clear()
   }
}
  1. Um den Click-Handler mit der Benutzeroberfläche zu verbinden, öffnen Sie fragment_sleep_tracker.xml und fügen Sie den Click-Handler zu clear_button hinzu.
android:onClick="@{() -> sleepTrackerViewModel.onClear()}"
  1. Erstellen Sie Ihre App und führen Sie sie aus.
  2. Tippen Sie auf Löschen, um alle Daten zu entfernen. Tippe dann auf Starten und Beenden, um neue Daten zu erstellen.

Android Studio-Projekt: TrackMySleepQualityCoroutines

  • Verwenden Sie ViewModel, ViewModelFactory und die Datenbindung, um die UI-Architektur für die App einzurichten.
  • Damit die Benutzeroberfläche reibungslos funktioniert, sollten Sie für zeitaufwendige Aufgaben wie alle Datenbankvorgänge Coroutinen verwenden.
  • Coroutinen sind asynchron und nicht blockierend. Sie verwenden suspend-Funktionen, um asynchronen Code sequenziell zu machen.
  • 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, sondern die Ausführung wird angehalten, bis das Ergebnis verfügbar ist. Anschließend wird die Ausführung mit dem Ergebnis fortgesetzt.
  • Der Unterschied zwischen Blockieren und Anhalten besteht darin, dass bei einem blockierten Thread keine weiteren Aktionen ausgeführt werden. Wenn der Thread angehalten wird, werden andere Aufgaben ausgeführt, bis das Ergebnis verfügbar ist.

Zum Starten einer Coroutine benötigen Sie einen Job, einen Dispatcher und einen Bereich:

  • Ein Job ist im Grunde alles, was abgebrochen werden kann. Jede Coroutine hat einen Job und Sie können einen Job verwenden, um eine Coroutine abzubrechen.
  • Der Dispatcher sendet Coroutinen, die in verschiedenen Threads ausgeführt werden. Dispatcher.Main führt Aufgaben im Hauptthread aus und Dispartcher.IO dient dazu, blockierende E/A-Aufgaben an einen gemeinsamen Thread-Pool auszulagern.
  • Der Scope kombiniert Informationen, einschließlich eines Jobs und eines Dispatchers, um den Kontext zu definieren, in dem die Coroutine ausgeführt wird. Mit Scopes werden Coroutinen im Blick behalten.

So implementieren Sie Klick-Handler, die Datenbankvorgänge auslösen:

  1. Starten Sie eine Coroutine, die im Haupt- oder UI-Thread ausgeführt wird, da sich das Ergebnis auf die Benutzeroberfläche auswirkt.
  2. Rufen Sie eine Suspend-Funktion auf, um die zeitaufwendige Arbeit zu erledigen, damit der UI-Thread nicht blockiert wird, während Sie auf das Ergebnis warten.
  3. Die lang andauernde Arbeit hat nichts mit der Benutzeroberfläche zu tun. Wechseln Sie also zum E/A-Kontext. So kann die Arbeit in einem Threadpool ausgeführt werden, der für diese Art von Vorgängen optimiert und reserviert ist.
  4. Rufen Sie dann die Datenbankfunktion auf, um die Aufgabe auszuführen.

Verwenden Sie eine Transformations-Zuordnung, um jedes Mal, wenn sich das Objekt ändert, einen String aus einem LiveData-Objekt zu erstellen.

Udacity-Kurs:

Android-Entwicklerdokumentation:

Weitere Dokumentation und Artikel:

In diesem Abschnitt werden mögliche Hausaufgaben für Schüler und Studenten aufgeführt, die dieses Codelab im Rahmen eines von einem Kursleiter geleiteten Kurses durcharbeiten. Es liegt in der Verantwortung des Kursleiters, Folgendes zu tun:

  • Weisen Sie bei Bedarf Aufgaben zu.
  • Teilen Sie den Schülern/Studenten mit, wie sie Hausaufgaben abgeben können.
  • Benoten Sie die Hausaufgaben.

Lehrkräfte können diese Vorschläge nach Belieben nutzen und auch andere Hausaufgaben zuweisen, die sie für angemessen halten.

Wenn Sie dieses Codelab selbst durcharbeiten, können Sie mit diesen Hausaufgaben Ihr Wissen testen.

Beantworten Sie diese Fragen

Frage 1

Welche der folgenden Optionen sind Vorteile von Coroutinen?

  • Sie sind nicht blockierend.
  • Sie werden asynchron ausgeführt.
  • Sie können in einem anderen Thread als dem Haupt-Thread ausgeführt werden.
  • Dadurch laufen Apps immer schneller.
  • Mit Ausnahmen
  • Sie können als linearer Code geschrieben und gelesen werden.

Frage 2

Was ist eine Suspend-Funktion?

  • Eine normale Funktion, die mit dem Keyword suspend annotiert ist.
  • Eine Funktion, die in Koroutinen aufgerufen werden kann.
  • Während eine suspend-Funktion ausgeführt wird, wird der aufrufende Thread angehalten.
  • Suspend-Funktionen müssen immer im Hintergrund ausgeführt werden.

Frage 3

Was ist der Unterschied zwischen dem Blockieren und dem Sperren eines Threads? Wählen Sie alle zutreffenden Aussagen aus.

  • Wenn die Ausführung blockiert ist, kann auf dem blockierten Thread keine andere Arbeit ausgeführt werden.
  • Wenn die Ausführung angehalten wird, kann der Thread andere Aufgaben ausführen, während er auf den Abschluss der ausgelagerten Arbeit wartet.
  • Das Anhalten ist effizienter, da Threads nicht warten und nichts tun müssen.
  • Unabhängig davon, ob die Ausführung blockiert oder ausgesetzt ist, wird weiterhin auf das Ergebnis der Coroutine gewartet, bevor fortgefahren wird.

Nächste Lektion: 6.3 LiveData zum Steuern von Schaltflächenstatus verwenden

Links zu anderen Codelabs in diesem Kurs finden Sie auf der Landingpage für Android Kotlin Fundamentals-Codelabs.