Android Room mit einer Ansicht – Kotlin

Der Zweck von Architekturkomponenten ist es, Anleitungen zur Anwendungsarchitektur zu geben, mit Bibliotheken für gängige Aufgaben wie die Lebenszyklusverwaltung und Datenpersistenz. Mithilfe von Architekturkomponenten können Sie Ihre App so stabil strukturieren, testen und verwalten, dass sie keinen Standardtext haben. Die Architekturkomponenten sind Teil von Android Jetpack.

Dies ist die Kotlin-Version des Codelabs. Die Version der Programmiersprache Java finden Sie hier.

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.

Voraussetzungen

Außerdem sollten Sie mit Kotlin, objektorientierten Designkonzepten und Android-Entwicklungsgrundsätzen vertraut sein, insbesondere mit folgenden Themen:

Es ist außerdem hilfreich, sich mit Softwarearchitekturmustern zu vertraut zu machen, mit denen Daten von der Benutzeroberfläche wie MVP oder MVC getrennt werden. In diesem Codelab wird die im Leitfaden zur App-Architektur definierte Architektur implementiert.

Dieses Codelab richtet sich an Android-Architekturkomponenten. Themen und Code, der nicht relevant ist, können Sie ganz einfach kopieren und einfügen.

Falls Sie mit Kotlin nicht vertraut sind, finden Sie hier eine Version dieses Codelabs in der Programmiersprache Java.

Aufgabe

In diesem Codelab lernen Sie, wie Sie eine Anwendung im Architekturraum, „ViewModel“ und „LiveData“ entwerfen und erstellen und wie Sie eine App erstellen, die folgende Vorteile bietet:

  • Implementiert unsere empfohlene Architektur unter Verwendung der Android-Architekturkomponenten
  • Die Funktion ist mit einer Datenbank verbunden, sodass die Daten abgerufen und gespeichert werden können. Außerdem werden in der Datenbank vorab einige Wörter angegeben.
  • Zeigt alle Wörter eines RecyclerView auf MainActivity an.
  • Öffnet eine zweite Aktivität, wenn der Nutzer auf die +-Schaltfläche tippt. Wenn der Nutzer ein Wort eingibt, wird es in die Datenbank und die Liste aufgenommen.

Die App ist einfach, aber ausreichend komplex und kann als Vorlage verwendet werden. Vorschau:

Voraussetzungen

  • Android Studio 3.0 oder höher und Anwendungsmöglichkeiten. Prüfe, ob Android Studio sowie dein SDK und Gradle aktualisiert werden.
  • Ein Android-Gerät oder Emulator.

Dieses Codelab umfasst den gesamten Code, der zum Erstellen der vollständigen App erforderlich ist.

Für die Verwendung der Architekturkomponenten und die Implementierung der empfohlenen Architektur sind zahlreiche Schritte erforderlich. Am wichtigsten ist es, ein mentales Modell zu erstellen. Sie sollten verstehen, wie die einzelnen Teile miteinander zusammenhängen und wie die Daten übertragen werden. Während Sie dieses Codelab durcharbeiten, sollten Sie den Code nicht einfach kopieren und einfügen, sondern versuchen, ein inneres Verständnis zu entwickeln.

Hier eine kurze Einführung in die Architekturkomponenten und ihre Kombination. Dieses Codelab konzentriert sich auf eine Teilmenge der Komponenten, nämlich „LiveData“, „ViewModel“ und „Room“. Jede Komponente wird im weiteren Verlauf genauer erklärt.

Dieses Diagramm zeigt eine grundlegende Form der Architektur:

Entität:Annotierte Klasse, mit der eine Datenbanktabelle beim Arbeiten mit Room beschrieben wird.

SQLite-Datenbank: Auf dem Gerätespeicher Die Persistenzbibliothek wird für Sie erstellt und verwaltet.

DAO: Datenzugriffsobjekt. Eine Zuordnung von SQL-Abfragen zu Funktionen. Wenn Sie einen DAO verwenden, rufen Sie die Methoden auf und Room erledigt den Rest.

Raumdatenbank: vereinfacht die Datenbankarbeit und dient als Zugangspunkt für die zugrunde liegende SQLite-Datenbank (SQLiteOpenHelper) wird ausgeblendet). Die Room-Datenbank verwendet den DAO, um Abfragen an die SQLite-Datenbank zu senden.

Repository: Eine von Ihnen erstellte Klasse, die in erster Linie zum Verwalten mehrerer Datenquellen verwendet wird.

ViewModel:Er dient als Kommunikationszentrum zwischen dem Repository (Daten) und der Benutzeroberfläche. Das UI muss sich nicht mehr um den Ursprung der Daten kümmern. ViewModel-Instanzen laufen über die Aktivität/Fragmentregeneration hinaus.

LiveData: Eine Datenhalterklasse, die beobachtbar ist. Die aktuelle Version der Daten wird immer in der Warteschleife gespeichert oder im Cache gespeichert. Die Betrachter werden benachrichtigt, wenn sich die Daten geändert haben. LiveData ist für den Lebenszyklus zuständig. Über die Benutzeroberflächenkomponenten werden nur relevante Daten beobachtet und die Beobachtung wird nicht gestoppt oder fortgesetzt. LiveData verwaltet automatisch all dies, da es die Änderungen des Lebenszyklusstatus im Blick behält.

RoomWordSample-Architektur

Das folgende Diagramm zeigt alle Teile der App. Jedes der umschließenden Felder (außer der SQLite-Datenbank) stellt eine von Ihnen erstellte Klasse dar.

  1. Öffnen Sie Android Studio und klicken Sie auf Start a new Android Studio project.
  2. Wählen Sie im Fenster „Neues Projekt erstellen“ die Option Leere Aktivität aus und klicken Sie auf Weiter.
  3. Geben Sie dem App-Bildschirm den Namen „RoomWordSample“ und klicken Sie auf Finish (Fertigstellen).

Als Nächstes müssen Sie Ihren Gradle-Dateien die Komponentenbibliotheken hinzufügen.

  1. Klicken Sie in Android Studio auf den Tab „Projekte“ und maximieren Sie den Ordner „Gradle Scripts“.

Öffne build.gradle (Modul: app).

  1. Wenden Sie das kapt-Kotlin-Plug-in für Annotationen an, indem Sie es nach den anderen Plug-ins einfügen, die oben in der Datei build.gradle (Modul: App) definiert wurden.
apply plugin: 'kotlin-kapt'
  1. Fügen Sie den Block packagingOptions in den Block android ein, um das Modul „Atomare Funktionen“ aus dem Paket auszuschließen und Warnungen zu vermeiden.
android {
    // other configuration (buildTypes, defaultConfig, etc.)

    packagingOptions {
        exclude 'META-INF/atomicfu.kotlin_module'
    }
}
  1. Fügen Sie den folgenden Code am Ende des Blocks dependencies ein.
// Room components
implementation "androidx.room:room-runtime:$rootProject.roomVersion"
kapt "androidx.room:room-compiler:$rootProject.roomVersion"
androidTestImplementation "androidx.room:room-testing:$rootProject.roomVersion"

// Lifecycle components
implementation "androidx.lifecycle:lifecycle-extensions:$rootProject.archLifecycleVersion"
kapt "androidx.lifecycle:lifecycle-compiler:$rootProject.archLifecycleVersion"
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$rootProject.archLifecycleVersion"

// Kotlin components
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
api "org.jetbrains.kotlinx:kotlinx-coroutines-core:$rootProject.coroutines"
api "org.jetbrains.kotlinx:kotlinx-coroutines-android:$rootProject.coroutines"

// Material design
implementation "com.google.android.material:material:$rootProject.materialVersion"

// Testing
testImplementation 'junit:junit:4.12'
androidTestImplementation "androidx.arch.core:core-testing:$rootProject.coreTestingVersion"
  1. Fügen Sie in der Datei build.gradle (Projekt: RoomWordsSample) wie im Code unten die Versionsnummer am Ende der Datei hinzu.
ext {
    roomVersion = '2.2.5'
    archLifecycleVersion = '2.2.0'
    coreTestingVersion = '2.1.0'
    materialVersion = '1.1.0'
    coroutines = '1.3.4'
}

Die Daten für diese Anwendung bestehen aus Wörtern. Sie benötigen dazu eine einfache Tabelle:

In Chatroom können Sie Tabellen über eine Entität erstellen. Lass uns das jetzt tun.

  1. Erstelle eine neue Kotlin-Klassendatei mit dem Namen Word, die die Datenklasse Word enthält.
    In dieser Klasse wird die Entität beschrieben, die die SQLite-Tabelle für Ihre Wörter darstellt. Jedes Attribut in der Klasse stellt eine Spalte in der Tabelle dar. Der Chatroom wird diese Attribute letztendlich verwenden, um die Tabelle zu erstellen und Objekte aus Zeilen in der Datenbank zu instanziieren.

Hier ist der Code:

data class Word(val word: String)

Damit die Klasse Word für eine Raumdatenbank relevant ist, müssen Sie sie annotieren. Annotationen geben an, wie sich die einzelnen Teile dieser Klasse auf einen Eintrag in der Datenbank beziehen. Anhand dieser Informationen wird der Code generiert.

Wenn Sie die Anmerkung selbst eingeben, anstatt sie einzufügen, werden die Anmerkungsklassen in Android Studio automatisch importiert.

  1. Aktualisieren Sie Ihre Word-Klasse mit Annotationen wie im folgenden Code gezeigt:
@Entity(tableName = "word_table")
class Word(@PrimaryKey @ColumnInfo(name = "word") val word: String)

Schauen wir mal, was diese Annotationen bewirken:

  • @Entity(tableName = "word_table")
    Jede @Entity-Klasse stellt eine SQLite-Tabelle dar. Annotieren Sie die Klassendeklaration, um anzugeben, dass es sich um eine Entität handelt. Sie können den Namen der Tabelle angeben, wenn dieser sich vom Namen der Klasse unterscheiden soll. Dabei wird die Tabelle "word_table" genannt.
  • @PrimaryKey
    Jede Entität benötigt einen Primärschlüssel. Einfach ausgedrückt ist jedes Wort als eigener Primärschlüssel.
  • @ColumnInfo(name = "word")
    Gibt den Namen der Spalte in der Tabelle an, wenn er vom Namen der Mitgliedsvariablen abweichen soll. Damit wird die Spalte „"“ genannt.
  • Alle in der Datenbank gespeicherten Properties müssen öffentlich sichtbar sein. Das ist die Kotlin-Standardeinstellung.

Eine vollständige Liste der Annotationen finden Sie in der Referenz zum Zimmerpaket.

Was ist der DAO?

Im DAO (Data Access Object) geben Sie SQL-Abfragen an und verknüpfen sie mit Methodenaufrufen. Der Compiler überprüft die SQL-Abfragen und generiert Abfragen aus Annotationen zu häufigen Abfragen, z. B. zu @Insert. Der DAO verwendet den Chatroom, um eine saubere API für Ihren Code zu erstellen.

Der DAO muss eine Schnittstelle oder abstrakte Klasse sein.

Standardmäßig müssen alle Abfragen in einem separaten Thread ausgeführt werden.

Außerdem werden Koroutinen unterstützt, sodass Ihre Abfragen mit dem suspend-Modifikator annotiert und dann von einer Koroutine oder von einer anderen Sperrungsfunktion aufgerufen werden können.

DAO implementieren

Schreiben Sie einen DAO mit folgenden Abfragen:

  • Alle Wörter alphabetisch sortieren
  • Ein Wort einfügen
  • Alle Wörter löschen
  1. Erstellen Sie eine neue Kotlin-Klassendatei mit dem Namen WordDao.
  2. Kopieren Sie den folgenden Code und fügen Sie ihn in WordDao ein, damit die Importe bei Bedarf neu kompiliert werden.
@Dao
interface WordDao {

    @Query("SELECT * from word_table ORDER BY word ASC")
    fun getAlphabetizedWords(): List<Word>

    @Insert(onConflict = OnConflictStrategy.IGNORE)
    suspend fun insert(word: Word)

    @Query("DELETE FROM word_table")
    suspend fun deleteAll()
}

Sehen wir uns das Ganze genauer an:

  • WordDao ist eine Schnittstelle. DAOs müssen entweder Schnittstellen oder abstrakte Klassen sein.
  • In der Annotation @Dao wird sie als DAO-Klasse für den Chatroom identifiziert.
  • suspend fun insert(word: Word): Deklariert eine Sperrfunktion, um ein Wort einzufügen.
  • Die Annotation @Insert ist eine spezielle Anmerkung für DAO-Methoden, bei der Sie keine SQL-Informationen angeben müssen. Es gibt auch die Annotationen @Delete und @Update zum Löschen und Aktualisieren von Zeilen. Sie verwenden sie jedoch nicht in dieser App.
  • onConflict = OnConflictStrategy.IGNORE: Die ausgewählte Strategie „Konflikt“ ignoriert ein neues Wort, wenn es genau mit dem Wort in der Liste übereinstimmt. Weitere Informationen zu den verfügbaren Konfliktstrategien finden Sie in der Dokumentation.
  • suspend fun deleteAll(): Deklariert eine Sperrfunktion, um alle Wörter zu löschen.
  • Es gibt keine praktische Annotation zum Löschen mehrerer Elemente. Daher wird sie mit der generischen @Query kommentiert.
  • @Query("DELETE FROM word_table"): Für @Query muss eine SQL-Abfrage als Stringparameter für die Annotation angegeben werden, um komplexe Leseabfragen und andere Vorgänge zu ermöglichen.
  • fun getAlphabetizedWords(): List<Word>: Eine Methode, mit der alle Wörter abgerufen und List von Words zurückgegeben werden.
  • @Query("SELECT * from word_table ORDER BY word ASC"): Abfrage, die eine Liste von Wörtern in aufsteigender Reihenfolge zurückgibt.

Wenn sich Daten ändern, empfiehlt es sich, bestimmte Maßnahmen zu ergreifen, z. B. die aktualisierten Daten auf der Benutzeroberfläche anzeigen zu lassen. Sie müssen die Daten also beobachten, damit Sie reagieren können, wenn sie sich ändern.

Je nachdem, wie die Daten gespeichert werden, kann das schwierig sein. Wenn Sie Änderungen an Daten in mehreren Komponenten Ihrer App beobachten, können Sie explizite, starre Abhängigkeitspfade zwischen den Komponenten erstellen. Dadurch wird unter anderem das Testen und das Debuggen erschwert.

LiveData, eine Lebenszyklusklasse zur Datenbeobachtung, löst dieses Problem. Verwenden Sie einen Rückgabewert vom Typ LiveData in der Methodenbeschreibung. Der Chatroom generiert den erforderlichen Code, um die LiveData zu aktualisieren, wenn die Datenbank aktualisiert wird.

Ändern Sie in WordDao die Signatur der getAlphabetizedWords()-Methode, sodass die zurückgegebene List<Word> mit LiveData umschlossen wird.

   @Query("SELECT * from word_table ORDER BY word ASC")
   fun getAlphabetizedWords(): LiveData<List<Word>>

Später in diesem Codelab lassen sich Datenänderungen über eine Observer in MainActivity verfolgen.

Was ist eine Raumdatenbank?

  • Room ist eine Datenbankebene über einer SQLite-Datenbank.
  • Der Chatroom kümmert sich um alltägliche Aufgaben, die Sie bisher mit einem SQLiteOpenHelper durchgeführt haben.
  • Der DAO verwendet die anfragen, um Anfragen an die Datenbank zu senden.
  • Zur Vermeidung von schlechter UI-Leistung können Sie in Chatroom standardmäßig keine Abfragen im Hauptthread senden. Wenn Raumabfragen LiveData zurückgeben, werden die Abfragen automatisch in einem Hintergrundthread ausgeführt.
  • Room bietet Prüfungen zur Kompilierdauer von SQLite-Anweisungen.

Raumdatenbank implementieren

Die Chatroom-Datenbankklasse muss abstrakt sein und RoomDatabase erweitern. In der Regel benötigen Sie nur eine Instanz einer Room-Datenbank für die gesamte App.

Jetzt erstellen.

  1. Erstellen Sie eine Kotlin-Klassendatei mit dem Namen WordRoomDatabase und fügen Sie ihr folgenden Code hinzu:
// Annotates class to be a Room Database with a table (entity) of the Word class
@Database(entities = arrayOf(Word::class), version = 1, exportSchema = false)
public abstract class WordRoomDatabase : RoomDatabase() {

   abstract fun wordDao(): WordDao

   companion object {
        // Singleton prevents multiple instances of database opening at the
        // same time. 
        @Volatile
        private var INSTANCE: WordRoomDatabase? = null

        fun getDatabase(context: Context): WordRoomDatabase {
            val tempInstance = INSTANCE
            if (tempInstance != null) {
                return tempInstance
            }
            synchronized(this) {
                val instance = Room.databaseBuilder(
                        context.applicationContext,
                        WordRoomDatabase::class.java, 
                        "word_database"
                    ).build()
                INSTANCE = instance
                return instance
            }
        }
   }
}

Sieh dir diesen Code einmal an:

  • Die Datenbankklasse für Zimmer muss abstract sein und RoomDatabase erweitern
  • Sie versehen die Klasse mit einer Raumdatenbank mit @Database und verwenden die Annotationsparameter, um die Entitäten zu deklarieren, die zur Datenbank gehören, und die Versionsnummer festzulegen. Jede Entität entspricht einer Tabelle, die in der Datenbank erstellt wird. Datenbankmigrationen werden in diesem Codelab nicht berücksichtigt. Daher haben wir hier exportSchema auf „false“ gesetzt, um eine Build-Warnung zu vermeiden. Bei einer echten App sollten Sie ein Verzeichnis für Room festlegen, in dem das Schema exportiert werden soll, damit Sie das aktuelle Schema in Ihrem Versionsverwaltungssystem prüfen können.
  • Die Datenbank stellt DAOs durch eine abstrakte Methode nach Methode für jedes @Dao bereit.
  • Wir haben einen SingletonWordRoomDatabase, definiert, um zu verhindern, dass mehrere Instanzen der Datenbank gleichzeitig geöffnet werden.
  • getDatabase gibt den Singleton zurück. Sie erstellt die Datenbank zum ersten Mal, wenn auf sie zugegriffen wird, und erstellt mit Room Database so ein RoomDatabaseObjekt im Anwendungskontext aus der WordRoomDatabase Klasse "word_database".

Was ist ein Repository?

Bei einer Repository-Klasse wird der Zugriff auf mehrere Datenquellen abstrahiert. Das Repository ist nicht Teil der Bibliothek für Komponenten der Architektur. Es ist aber eine empfohlene Best Practice für die Code-Trennung und -Architektur. Eine Repository-Klasse bietet eine saubere API für den Datenzugriff auf den Rest der Anwendung.

Vorteile eines Repositorys

Ein Repository verwaltet Abfragen und ermöglicht Ihnen die Verwendung mehrerer Back-Ends. Im gängigsten Beispiel implementiert das Repository die Logik, um zu entscheiden, ob Daten aus einem Netzwerk abgerufen oder Ergebnisse in einer lokalen Datenbank im Cache verwendet werden sollen.

Repository implementieren

Erstellen Sie eine Kotlin-Klassendatei mit dem Namen WordRepository und fügen Sie den folgenden Code in sie ein:

// Declares the DAO as a private property in the constructor. Pass in the DAO
// instead of the whole database, because you only need access to the DAO
class WordRepository(private val wordDao: WordDao) {

    // Room executes all queries on a separate thread.
    // Observed LiveData will notify the observer when the data has changed.
    val allWords: LiveData<List<Word>> = wordDao.getAlphabetizedWords()
 
    suspend fun insert(word: Word) {
        wordDao.insert(word)
    }
}

Das Wichtigste in Kürze:

  • Der DAO wird im Gegensatz zu der gesamten Datenbank an den Repository-Konstruktor übergeben. Das liegt daran, dass er nur Zugriff auf den DAO benötigt, da er alle Lese-/Schreibmethoden für die Datenbank enthält. Es ist nicht nötig, die gesamte Datenbank für das Repository freizugeben.
  • Die Wortliste ist eine öffentliche Eigenschaft. Initialisiert wird er durch Abrufen der LiveData-Wörterliste aus dem Chatroom. Dies ist auf die Art und Weise zurückzuführen, wie die getAlphabetizedWords-Methode festgelegt wurde, dass LiveData im Schritt „LiveData“ zurückgegeben werden soll. Der Chatroom führt alle Abfragen in einem separaten Thread aus. Über den beobachteten Zustand LiveData wird der Beobachter im Haupt-Thread informiert, wenn sich die Daten geändert haben.
  • Der Modifikator suspend teilt dem Compiler mit, dass dies aus einer Koroutine oder einer anderen sperrenden Funktion aufgerufen werden muss.

Was ist ein ViewModel?

Die Rolle ViewModel dient dazu, Daten in der UI bereitzustellen und Konfigurationsänderungen zu überstehen. Ein ViewModel fungiert als Kommunikationszentrum zwischen dem Repository und der UI. Du kannst auch ViewModel verwenden, um Daten zwischen Fragmenten auszutauschen. Das ViewModel ist Teil der Lebenszyklusbibliothek.

Eine Einführung zu diesem Thema finden Sie im Blogpost ViewModel Overview oder im Blogpost ViewModels: A Simple Example.

Vorteile von ViewModel

Ein ViewModel enthält die UI-Daten Ihrer App auf Lebenszyklusebene und ohne Änderungen an der Konfiguration. Wenn du die UI-Daten deiner App von den Klassen Activity und Fragment trennst, kannst du besser ein einziges Prinzip anwenden: Deine Aktivitäten und Fragmente sind für die Darstellung von Daten auf dem Bildschirm verantwortlich, während dein ViewModel alle Daten verwaltet und verarbeitet, die für die Benutzeroberfläche erforderlich sind.

Verwende in der ViewModel LiveData für veränderbare Daten, die von der UI verwendet oder angezeigt werden. Die Verwendung von LiveData bietet mehrere Vorteile:

  • Anstatt Daten zu abfragen, können Sie einen Beobachter für die Daten hinzufügen und die Benutzeroberfläche
    nur dann aktualisieren, wenn sich die Daten tatsächlich ändern.
  • Das Repository und die UI sind vollständig durch ViewModel getrennt.
  • Es gibt keine Datenbankaufrufe aus dem ViewModel. Diese Vorgänge werden im Repository verarbeitet, sodass der Code testbar ist.

viewModelScope

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.

Die AndroidX-Bibliothek lifecycle-viewmodel-ktx fügt viewModelScope als Erweiterungsfunktion der Klasse ViewModel hinzu, sodass du mit Bereichen arbeiten kannst.

Weitere Informationen zur Verwendung von Koroutinen im ViewModel finden Sie im Codelab im Codelab zu Verwendung von Kotlin Coroutinen in Ihrer Android-App oder im Blogpost „Easy Coroutines in Android: viewModelScope“.

ViewModel implementieren

Erstellen Sie eine Kotlin-Klassendatei für WordViewModel und fügen Sie ihr folgenden Code hinzu:

class WordViewModel(application: Application) : AndroidViewModel(application) {

    private val repository: WordRepository
    // Using LiveData and caching what getAlphabetizedWords returns has several benefits:
    // - We can put an observer on the data (instead of polling for changes) and only update the
    //   the UI when the data actually changes.
    // - Repository is completely separated from the UI through the ViewModel.
    val allWords: LiveData<List<Word>>

    init {
        val wordsDao = WordRoomDatabase.getDatabase(application).wordDao()
        repository = WordRepository(wordsDao)
        allWords = repository.allWords
    }

    /**
     * Launching a new coroutine to insert the data in a non-blocking way
     */
    fun insert(word: Word) = viewModelScope.launch(Dispatchers.IO) {
        repository.insert(word)
    }
}

Das haben wir:

  • Sie haben die Klasse WordViewModel erstellt, die Application als Parameter erhält und AndroidViewModel erweitert.
  • Eine private Mitgliedsvariable für einen Verweis auf das Repository wurde hinzugefügt.
  • Es wurde eine öffentliche LiveData-Mitgliedsvariable hinzugefügt, um die Wortliste im Cache zu speichern.
  • Es wurde ein init-Block erstellt, der einen Verweis auf den WordDao aus der WordRoomDatabase erhält.
  • Im init-Block wurde der WordRepository basierend auf dem WordRoomDatabase erstellt.
  • Im init-Block wurden die allWords-LiveData mithilfe des Repositorys initialisiert.
  • Es wurde eine Wrapper-insert()-Methode erstellt, die die Repository-Methode insert() aufruft. So wird die Implementierung von insert() von der Benutzeroberfläche eingekapselt. Wir möchten nicht zum Einfügen des Haupt-Threads einfügen, deshalb starten wir eine neue Koroutine und rufen die Repository-Einfügenfunktion ein. Diese ist eine Sperrfunktion. Wie bereits erwähnt, umfassen ViewModels auf ihrem Lebenszyklus viewModelScopeeinen Koroutine-Bereich, den wir hier verwenden.

Als Nächstes müssen Sie das XML-Layout für die Liste und die Elemente hinzufügen.

In diesem Codelab wird davon ausgegangen, dass Sie mit dem Erstellen von Layouts in XML vertraut sind. Deshalb erhalten Sie nur den Code.

Machen Sie das Material für das Anwendungsdesign möglich, indem Sie für das übergeordnete Element AppTheme den Wert Theme.MaterialComponents.Light.DarkActionBar festlegen. Fügen Sie einen Stil für Listenelemente in values/styles.xml hinzu:

<resources>

    <!-- Base application theme. -->
    <style name="AppTheme" parent="Theme.MaterialComponents.Light.DarkActionBar">
        <!-- Customize your theme here. -->
        <item name="colorPrimary">@color/colorPrimary</item>
        <item name="colorPrimaryDark">@color/colorPrimaryDark</item>
        <item name="colorAccent">@color/colorAccent</item>
    </style>

    <!-- The default font for RecyclerView items is too small.
    The margin is a simple delimiter between the words. -->
    <style name="word_title">
        <item name="android:layout_width">match_parent</item>
        <item name="android:layout_marginBottom">8dp</item>
        <item name="android:paddingLeft">8dp</item>
        <item name="android:background">@android:color/holo_orange_light</item>
        <item name="android:textAppearance">@android:style/TextAppearance.Large</item>
    </style>
</resources>

Fügen Sie ein layout/recyclerview_item.xml-Layout hinzu:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical" 
    android:layout_width="match_parent"
    android:layout_height="wrap_content">

    <TextView
        android:id="@+id/textView"
        style="@style/word_title"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="@android:color/holo_orange_light" />
</LinearLayout>

Ersetzen Sie in layout/activity_main.xml den TextView durch ein RecyclerView und fügen Sie eine unverankerte Aktionsschaltfläche (FAB) hinzu. Das Layout sollte nun so aussehen:

<androidx.constraintlayout.widget.ConstraintLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/recyclerview"
        android:layout_width="0dp"
        android:layout_height="0dp"
        tools:listitem="@layout/recyclerview_item"
        android:padding="@dimen/big_padding"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <com.google.android.material.floatingactionbutton.FloatingActionButton
        android:id="@+id/fab"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_margin="16dp"
        android:contentDescription="@string/add_word"/>

</androidx.constraintlayout.widget.ConstraintLayout>

Die UAS-Darstellung sollte der verfügbaren Aktion entsprechen. Daher sollten wir das Symbol durch ein +-Symbol ersetzen.

Zuerst müssen Sie ein neues Vektor-Asset hinzufügen.

  1. Wählen Sie Datei > Neues > Vektor-Asset aus.
  2. Klicken Sie im Feld Clip Art: auf das Android-Robot-Symbol.
  3. Suche nach „Add“ und wähle das Asset „'+'“ aus. Klicken Sie auf OK
    .
  4. Klicken Sie anschließend auf Weiter.
  5. Bestätigen Sie den Symbolpfad als main > drawable und klicken Sie auf Finish (Fertig), um das Asset hinzuzufügen.
  6. Aktualisieren Sie die UAS immer in layout/activity_main.xml, um die neue Drawable hinzuzufügen:
<com.google.android.material.floatingactionbutton.FloatingActionButton
        android:id="@+id/fab"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_margin="16dp"
        android:contentDescription="@string/add_word"
        android:src="@drawable/ic_add_black_24dp"/>

Du zeigst die Daten in einem RecyclerView an. Das ist etwas freundlicher, als nur die Daten in einem TextView zu werfen. In diesem Codelab wird davon ausgegangen, dass Sie wissen, wie RecyclerView, RecyclerView.LayoutManager, RecyclerView.ViewHolder und RecyclerView.Adapter funktionieren.

Die Variable words im Adapter speichert die Daten im Cache. In der nächsten Aufgabe fügen Sie den Code hinzu, der die Daten automatisch aktualisiert.

Erstellen Sie eine Kotlin-Klassendatei für WordListAdapter, die RecyclerView.Adapter erweitert. Hier ist der Code:

class WordListAdapter internal constructor(
        context: Context
) : RecyclerView.Adapter<WordListAdapter.WordViewHolder>() {

    private val inflater: LayoutInflater = LayoutInflater.from(context)
    private var words = emptyList<Word>() // Cached copy of words

    inner class WordViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
        val wordItemView: TextView = itemView.findViewById(R.id.textView)
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): WordViewHolder {
        val itemView = inflater.inflate(R.layout.recyclerview_item, parent, false)
        return WordViewHolder(itemView)
    }

    override fun onBindViewHolder(holder: WordViewHolder, position: Int) {
        val current = words[position]
        holder.wordItemView.text = current.word
    }

    internal fun setWords(words: List<Word>) {
        this.words = words
        notifyDataSetChanged()
    }

    override fun getItemCount() = words.size
}

Füge RecyclerView in der Methode onCreate() von MainActivity hinzu.

Gehen Sie in der onCreate()-Methode nach setContentView so vor:

   val recyclerView = findViewById<RecyclerView>(R.id.recyclerview)
   val adapter = WordListAdapter(this)
   recyclerView.adapter = adapter
   recyclerView.layoutManager = LinearLayoutManager(this)

Führen Sie Ihre App aus, um sicherzustellen, dass alles funktioniert. Es sind keine Elemente vorhanden, da Sie die Daten noch nicht angeschlossen haben.

In der Datenbank sind keine Daten vorhanden. Daten werden auf zwei Arten hinzugefügt: Beim Öffnen der Datenbank müssen Sie einige Daten hinzufügen. Wenn Sie Wörter hinzufügen möchten, fügen Sie ein Activity hinzu.

Wenn Sie alle Inhalte löschen und die Datenbank beim Starten der App neu füllen möchten, erstellen Sie ein RoomDatabase.Callback und überschreiben onOpen(). Da Sie Raumdatenbankvorgänge im UI-Thread nicht ausführen können, startet onOpen() eine Koroutine im E/A-Absender.

Zum Starten einer Koroutine benötigen Sie eine CoroutineScope. Aktualisiere die getDatabase-Methode der WordRoomDatabase-Klasse, um auch einen Koroutine-Bereich als Parameter zu erhalten:

fun getDatabase(
       context: Context,
       scope: CoroutineScope
  ): WordRoomDatabase {
...
}

Aktualisiere den Datenbankabrufbereiter im init-Block von WordViewModel, um auch den Bereich zu übergeben:

val wordsDao = WordRoomDatabase.getDatabase(application, viewModelScope).wordDao()

In WordRoomDatabase erstellen wir eine benutzerdefinierte Implementierung von RoomDatabase.Callback(), die auch einen CoroutineScope als Konstruktorparameter erhält. Dann überschreiben wir die onOpen-Methode, um die Datenbank zu befüllen.

Code für den Callback in der Klasse WordRoomDatabase:

private class WordDatabaseCallback(
    private val scope: CoroutineScope
) : RoomDatabase.Callback() {

    override fun onOpen(db: SupportSQLiteDatabase) {
        super.onOpen(db)
        INSTANCE?.let { database ->
            scope.launch {
                populateDatabase(database.wordDao())
            }
        }
    }

    suspend fun populateDatabase(wordDao: WordDao) {
        // Delete all content here.
        wordDao.deleteAll()

        // Add sample words.
        var word = Word("Hello")
        wordDao.insert(word)
        word = Word("World!")
        wordDao.insert(word)

        // TODO: Add your own words!
    }
}

Fügen Sie schließlich den Callback zur Build-Sequenz für die Datenbank hinzu, bevor Sie .build() für Room.databaseBuilder() aufrufen:

.addCallback(WordDatabaseCallback(scope))

Der endgültige Code sollte so aussehen:

@Database(entities = arrayOf(Word::class), version = 1, exportSchema = false)
abstract class WordRoomDatabase : RoomDatabase() {

   abstract fun wordDao(): WordDao

   private class WordDatabaseCallback(
       private val scope: CoroutineScope
   ) : RoomDatabase.Callback() {

       override fun onOpen(db: SupportSQLiteDatabase) {
           super.onOpen(db)
           INSTANCE?.let { database ->
               scope.launch {
                   var wordDao = database.wordDao()

                   // Delete all content here.
                   wordDao.deleteAll()

                   // Add sample words.
                   var word = Word("Hello")
                   wordDao.insert(word)
                   word = Word("World!")
                   wordDao.insert(word)

                   // TODO: Add your own words!
                   word = Word("TODO!")
                   wordDao.insert(word)
               }
           }
       }
   }

   companion object {
       @Volatile
       private var INSTANCE: WordRoomDatabase? = null

       fun getDatabase(
           context: Context,
           scope: CoroutineScope
       ): WordRoomDatabase {
            // if the INSTANCE is not null, then return it,
            // if it is, then create the database
            return INSTANCE ?: synchronized(this) {
                val instance = Room.databaseBuilder(
                        context.applicationContext,
                        WordRoomDatabase::class.java,
                        "word_database"
                )
                 .addCallback(WordDatabaseCallback(scope))
                 .build()
                INSTANCE = instance
                // return instance
                instance
        }
     }
   }
}

Fügen Sie diese String-Ressourcen in values/strings.xml hinzu:

<string name="hint_word">Word...</string>
<string name="button_save">Save</string>
<string name="empty_not_saved">Word not saved because it is empty.</string>

Diese Farbressource in value/colors.xml hinzufügen:

<color name="buttonLabel">#FFFFFF</color>

Erstellen Sie eine neue Dimensionsressourcendatei:

  1. Klicken Sie im Fenster Projekt auf das App-Modul.
  2. Wähle File > New > Android Resource File aus
  3. Wählen Sie unter „Verfügbare Kennzeichner“ die Option Dimension aus.
  4. Dateinamen festlegen: abdunkelt

Fügen Sie diese Dimensionsressourcen in values/dimens.xml hinzu:

<dimen name="small_padding">8dp</dimen>
<dimen name="big_padding">16dp</dimen>

Erstelle mit der Vorlage für leere Aktivitäten eine neue leere Android-App: Activity:

  1. Wählen Sie Datei > Aktivität > Leere Aktivität aus.
  2. Geben Sie als Aktivitätsnamen NewWordActivity ein.
  3. Überprüfe, ob die neue Aktivität dem Android-Manifest hinzugefügt wurde.
<activity android:name=".NewWordActivity"></activity>

Aktualisiere die Datei activity_new_word.xml im Layoutordner mit dem folgenden Code:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <EditText
        android:id="@+id/edit_word"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:minHeight="@dimen/min_height"
        android:fontFamily="sans-serif-light"
        android:hint="@string/hint_word"
        android:inputType="textAutoComplete"
        android:layout_margin="@dimen/big_padding"
        android:textSize="18sp" />

    <Button
        android:id="@+id/button_save"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="@color/colorPrimary"
        android:text="@string/button_save"
        android:layout_margin="@dimen/big_padding"
        android:textColor="@color/buttonLabel" />

</LinearLayout>

Aktualisieren Sie den Code für die Aktivität:

class NewWordActivity : AppCompatActivity() {

    private lateinit var editWordView: EditText

    public override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_new_word)
        editWordView = findViewById(R.id.edit_word)

        val button = findViewById<Button>(R.id.button_save)
        button.setOnClickListener {
            val replyIntent = Intent()
            if (TextUtils.isEmpty(editWordView.text)) {
                setResult(Activity.RESULT_CANCELED, replyIntent)
            } else {
                val word = editWordView.text.toString()
                replyIntent.putExtra(EXTRA_REPLY, word)
                setResult(Activity.RESULT_OK, replyIntent)
            }
            finish()
        }
    }

    companion object {
        const val EXTRA_REPLY = "com.example.android.wordlistsql.REPLY"
    }
}

Im letzten Schritt wird die Benutzeroberfläche mit der Datenbank verbunden, indem neue Wörter gespeichert werden, die der Nutzer eingibt, sowie der aktuelle Inhalt der Wortdatenbank in RecyclerView.

Fügen Sie einen Beobachter hinzu, der das LiveData in der ViewModel beobachtet, um den aktuellen Inhalt der Datenbank anzuzeigen.

Bei jeder Änderung der Daten wird der Callback onChanged() aufgerufen, der die Methode setWords() des Adapters aufruft, um die im Cache gespeicherten Daten zu aktualisieren und die angezeigte Liste zu aktualisieren.

Erstellen Sie in MainActivity eine Mitgliedervariable für die ViewModel:

private lateinit var wordViewModel: WordViewModel

Verwende ViewModelProvider, um dein ViewModel mit deinem Activity zu verknüpfen.

Wenn dein Activity erstmals beginnt, erstellt der ViewModelProviders die ViewModel. Wenn die Aktivität gelöscht wird, z. B. durch eine Konfigurationsänderung, bleibt ViewModel erhalten. Wenn die Aktivität neu erstellt wird, gibt ViewModelProviders den vorhandenen ViewModel zurück. Weitere Informationen findest du unter ViewModel.

Rufen Sie in onCreate() unter dem Codeblock RecyclerView einen ViewModel aus dem ViewModelProvider auf:

wordViewModel = ViewModelProvider(this).get(WordViewModel::class.java)

Fügen Sie außerdem in onCreate() einen Beobachter für die Property „allWords“ LiveData aus WordViewModel hinzu.

Die Methode onChanged() (die Standardmethode für unseren Lambda) wird ausgelöst, wenn sich die beobachteten Daten ändern und die Aktivität im Vordergrund ausgeführt wird:

wordViewModel.allWords.observe(this, Observer { words ->
            // Update the cached copy of the words in the adapter.
            words?.let { adapter.setWords(it) }
})

Wir möchten den NewWordActivity öffnen, wenn wir auf die UAS tippen. Sobald wir wieder in MainActivity angemeldet sind, fügen wir das neue Wort in die Datenbank ein oder zeigen eine Toast an. Zuerst legen wir dazu einen Anfragecode fest:

private val newWordActivityRequestCode = 1

Füge in MainActivity den onActivityResult()-Code für NewWordActivity hinzu.

Wenn die Aktivität mit RESULT_OK zurückgegeben wird, fügen Sie das zurückgegebene Wort in die Datenbank ein, indem Sie die insert()-Methode von WordViewModel aufrufen:

override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
    super.onActivityResult(requestCode, resultCode, data)

    if (requestCode == newWordActivityRequestCode && resultCode == Activity.RESULT_OK) {
        data?.getStringExtra(NewWordActivity.EXTRA_REPLY)?.let {
            val word = Word(it)
            wordViewModel.insert(word)
        }
    } else {
        Toast.makeText(
            applicationContext,
            R.string.empty_not_saved,
            Toast.LENGTH_LONG).show()
    }
}

Starte in MainActivity, NewWordActivity, wenn der Nutzer auf den UAS tippt. Suchen Sie in der MainActivity onCreate nach der UAS und fügen Sie eine onClickListener mit diesem Code hinzu:

val fab = findViewById<FloatingActionButton>(R.id.fab)
fab.setOnClickListener {
  val intent = Intent(this@MainActivity, NewWordActivity::class.java)
  startActivityForResult(intent, newWordActivityRequestCode)
}

Der fertige Code sollte so aussehen:

class MainActivity : AppCompatActivity() {

   private const val newWordActivityRequestCode = 1
   private lateinit var wordViewModel: WordViewModel

   override fun onCreate(savedInstanceState: Bundle?) {
       super.onCreate(savedInstanceState)
       setContentView(R.layout.activity_main)
       setSupportActionBar(toolbar)

       val recyclerView = findViewById<RecyclerView>(R.id.recyclerview)
       val adapter = WordListAdapter(this)
       recyclerView.adapter = adapter
       recyclerView.layoutManager = LinearLayoutManager(this)

       wordViewModel = ViewModelProvider(this).get(WordViewModel::class.java)
       wordViewModel.allWords.observe(this, Observer { words ->
           // Update the cached copy of the words in the adapter.
           words?.let { adapter.setWords(it) }
       })

       val fab = findViewById<FloatingActionButton>(R.id.fab)
       fab.setOnClickListener {
           val intent = Intent(this@MainActivity, NewWordActivity::class.java)
           startActivityForResult(intent, newWordActivityRequestCode)
       }
   }

   override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
       super.onActivityResult(requestCode, resultCode, data)

       if (requestCode == newWordActivityRequestCode && resultCode == Activity.RESULT_OK) {
           data?.getStringExtra(NewWordActivity.EXTRA_REPLY)?.let {
               val word = Word(it)
               wordViewModel.insert(word)
           }
       } else {
           Toast.makeText(
               applicationContext,
               R.string.empty_not_saved,
               Toast.LENGTH_LONG).show()
       }
   }
}

Führen Sie nun Ihre Anwendung aus. Wenn Sie der Datenbank ein Wort in NewWordActivity hinzufügen, wird die Benutzeroberfläche automatisch aktualisiert.

Du hast jetzt eine funktionierende App. Fassen wir noch einmal zusammen, was du erstellt hast. Hier noch einmal die App-Struktur:

Komponenten der Anwendung:

  • MainActivity: zeigt Wörter in einer Liste mit RecyclerView und WordListAdapter an In MainActivity gibt es einen Observer, der die Wörter „LiveData“ aus der Datenbank beobachtet und über Änderungen informiert wird.
  • NewWordActivity: fügt der Liste ein neues Wort hinzu.
  • WordViewModel: stellt Methoden für den Zugriff auf die Datenschicht bereit und gibt LiveData zurück, damit MainActivity die Observer-Beziehung einrichten kann.*
  • LiveData<List<Word>>: Ermöglicht die automatischen Updates in den Komponenten der Benutzeroberfläche Im MainActivity ist ein Observer enthalten, der die Wörter „LiveData“ aus der Datenbank beobachtet und über ihre Änderungen benachrichtigt wird.
  • Repository: verwaltet eine oder mehrere Datenquellen. Unter Repository sind Methoden für das ViewModel verfügbar, um mit dem zugrunde liegenden Datenanbieter zu interagieren. In dieser App ist das Back-End eine Room-Datenbank.
  • Room: ist ein Wrapper und implementiert eine SQLite-Datenbank. Chatrooms haben viel Arbeit für Sie, den Sie früher selbst machen mussten.
  • DAO: Ordnet Methodenaufrufe Datenbankabfragen zu. Wenn das Repository also eine Methode wie getAlphabetizedWords() aufruft, kann Room SELECT * from word_table ORDER BY word ASC ausführen.
  • Word: ist die Entitätsklasse, die ein einzelnes Wort enthält.

* Views und Activities (und Fragments) interagieren nur über die ViewModel mit den Daten. Daher spielt es keine Rolle, woher die Daten stammen.

Datenfluss für automatische UI-Updates (reaktive Benutzeroberfläche)

Das automatische Update ist möglich, weil wir LiveData verwenden. Im MainActivity ist ein Observer enthalten, der die Wörter „LiveData“ aus der Datenbank beobachtet und über ihre Änderungen benachrichtigt wird. Nach einer Änderung wird die Methode onChange() des Observers ausgeführt und mWords in WordListAdapter aktualisiert.

Die Daten können beobachtet werden, da sie LiveData enthalten. Außerdem wird die LiveData<List<Word>> beobachtet, die von der Property WordViewModel allWords zurückgegeben wird.

Über WordViewModel wird das gesamte Back-End in der UI-Ebene ausgeblendet. Es bietet Methoden für den Zugriff auf die Datenschicht und gibt LiveData zurück, damit MainActivity die Beobachtenbeziehung einrichten kann. Views und Activities (und Fragments) interagieren nur über die ViewModel mit den Daten. Daher spielt es keine Rolle, woher die Daten stammen.

In diesem Fall stammen die Daten aus einem Repository. Das ViewModel muss nicht wissen, mit was dieses Repository interagiert. Dazu muss es nur wissen, wie die Repository funktioniert. Die Methoden werden über die Repository bereitgestellt.

Im Repository wird mindestens eine Datenquelle verwaltet. In der WordListSample-Anwendung ist dieses Back-End eine Room-Datenbank. Room ist ein Wrapper und implementiert eine SQLite-Datenbank. Chatrooms haben viel Arbeit für Sie, den Sie früher selbst machen mussten. Beispiel: Der Chatroom macht alles, was Sie bisher mit einer SQLiteOpenHelper-Klasse gemacht haben.

Der DAO ordnet die Methoden Methoden zu. Wenn im Repository eine Methode wie getAllWords() aufgerufen wird, kann SELECT * from word_table ORDER BY word ASC von Room ausgeführt werden.

Weil das von der Abfrage zurückgegebene Ergebnis LiveData wird, wird jedes Mal, wenn sich die Daten in Room ändern, die Observer-Schnittstelle onChanged() ausgeführt und die UI aktualisiert.

[Optional] Lösungscode herunterladen

Wenn Sie sie noch nicht haben, können Sie sich den Lösungscode für das Codelab ansehen. Sie können sich das GitHub-Repository ansehen oder den Code hier herunterladen:

Quellcode herunterladen

Entpacken Sie die heruntergeladene ZIP-Datei. Dadurch wird der Stammordner android-room-with-a-view-kotlin entpackt, der die vollständige Anwendung enthält.