Architecture Components sollen eine Anleitung zur App-Architektur bieten. Sie enthalten Bibliotheken für gängige Aufgaben wie die Verwaltung des Lebenszyklus und die Datenpersistenz. Architekturkomponenten helfen Ihnen, Ihre App so zu strukturieren, dass sie robust, testbar und wartbar ist und weniger Boilerplate-Code enthält. Die Architecture Component-Bibliotheken sind Teil von Android Jetpack.
Dies ist die Kotlin-Version des Codelabs. Die Version in der Programmiersprache Java finden Sie hier.
Wenn Sie bei der Bearbeitung dieses Codelabs auf Probleme stoßen (z. B. Codefehler, Grammatikfehler oder unklare Formulierungen), melden Sie das Problem bitte über den Link Fehler melden unten links im Codelab.
Voraussetzungen
Sie sollten mit Kotlin, objektorientierten Designkonzepten und den Grundlagen der Android-Entwicklung vertraut sein, insbesondere mit:
RecyclerView
und Adapter- SQLite-Datenbank und die SQLite-Abfragesprache
- Grundlegende Coroutinen (Wenn Sie mit Coroutinen nicht vertraut sind, können Sie sich Kotlin-Coroutinen in Ihrer Android-App verwenden ansehen.)
Es ist auch hilfreich, mit Softwarearchitekturmustern vertraut zu sein, die Daten von der Benutzeroberfläche trennen, z. B. MVP oder MVC. In diesem Codelab wird die im Leitfaden zur App-Architektur definierte Architektur implementiert.
In diesem Codelab geht es um Android-Architekturkomponenten. Irrelevante Konzepte und Codeblöcke können Sie einfach kopieren und einfügen.
Wenn Sie mit Kotlin nicht vertraut sind, finden Sie hier eine Version dieses Codelabs in der Programmiersprache Java.
Aufgabe
In diesem Codelab erfahren Sie, wie Sie eine App mit den Architekturkomponenten Room, ViewModel und LiveData entwerfen und erstellen. Die App soll Folgendes leisten:
- Implementiert unsere empfohlene Architektur mit den Android-Architekturkomponenten.
- Die App arbeitet mit einer Datenbank, um Daten abzurufen und zu speichern. Die Datenbank wird mit einigen Wörtern vorab gefüllt.
- Zeigt alle Wörter in einem
RecyclerView
inMainActivity
an. - Öffnet eine zweite Aktivität, wenn der Nutzer auf die Schaltfläche „+“ tippt. Wenn der Nutzer ein Wort eingibt, wird es der Datenbank und der Liste hinzugefügt.
Die App ist einfach gehalten, aber komplex genug, um als Vorlage zu dienen. Hier eine Vorschau:
Voraussetzungen
- Android Studio ab Version 3.0 und Kenntnisse in der Verwendung von Android Studio. Achten Sie darauf, dass Android Studio sowie Ihr SDK und Gradle auf dem neuesten Stand sind.
- Ein Android-Gerät oder ein Android-Emulator.
In diesem Codelab finden Sie den gesamten Code, den Sie zum Erstellen der vollständigen App benötigen.
Die Verwendung der Architecture Components und die Implementierung der empfohlenen Architektur erfordert viele Schritte. Das Wichtigste ist, ein mentales Modell der Vorgänge zu erstellen, um zu verstehen, wie die einzelnen Teile zusammenpassen und wie die Daten fließen. Kopieren Sie den Code in diesem Codelab nicht einfach, sondern versuchen Sie, ihn zu verstehen.
Welche Architecture Components werden empfohlen?
Zur Einführung in die Terminologie finden Sie hier eine kurze Einführung in die Architecture Components und ihre Zusammenarbeit. In diesem Codelab wird nur auf eine Teilmenge der Komponenten eingegangen, nämlich LiveData, ViewModel und Room. Die einzelnen Komponenten werden während der Verwendung näher erläutert.
Das folgende Diagramm zeigt eine grundlegende Form der Architektur:
Entität:Annotierte Klasse, die eine Datenbanktabelle bei der Arbeit mit Room beschreibt.
SQLite-Datenbank:Speicher auf dem Gerät. Die Room-Persistenzbibliothek erstellt und verwaltet diese Datenbank für Sie.
DAO:Data Access Object (Datenzugriffsobjekt). Eine Zuordnung von SQL-Abfragen zu Funktionen. Wenn Sie ein DAO verwenden, rufen Sie die Methoden auf und Room erledigt den Rest.
Room-Datenbank:Vereinfacht die Datenbankarbeit und dient als Zugriffspunkt für die zugrunde liegende SQLite-Datenbank (verbirgt SQLiteOpenHelper)
). Die Room-Datenbank verwendet das DAO, um Abfragen an die SQLite-Datenbank zu senden.
Repository:Eine von Ihnen erstellte Klasse, die hauptsächlich zum Verwalten mehrerer Datenquellen verwendet wird.
ViewModel:Fungiert als Kommunikationszentrum zwischen dem Repository (Daten) und der Benutzeroberfläche. Die Benutzeroberfläche muss sich nicht mehr um den Ursprung der Daten kümmern. ViewModel-Instanzen überdauern die Neuerstellung von Aktivitäten/Fragmenten.
LiveData: Eine Datenhalterklasse, die beobachtet werden kann. Speichert immer die neueste Version der Daten im Cache und benachrichtigt die Beobachter, wenn sich die Daten geändert haben. LiveData
ist lebenszyklusbezogen. UI-Komponenten beobachten nur relevante Daten und unterbrechen oder setzen die Beobachtung nicht fort. LiveData übernimmt die Verwaltung all dieser Aspekte automatisch, da es die relevanten Änderungen des Lebenszyklusstatus während der Beobachtung erkennt.
Übersicht über die Architektur von RoomWordSample
Das folgende Diagramm zeigt alle Teile der App. Jedes der umschließenden Rechtecke (mit Ausnahme der SQLite-Datenbank) stellt eine Klasse dar, die Sie erstellen werden.
- Öffnen Sie Android Studio und klicken Sie auf Start a new Android Studio project (Neues Android Studio-Projekt starten).
- Wählen Sie im Fenster „Neues Projekt erstellen“ die Option Leere Aktivität aus und klicken Sie auf Weiter.
- Geben Sie auf dem nächsten Bildschirm den Namen „RoomWordSample“ für die App ein und klicken Sie auf Fertig stellen.
Als Nächstes müssen Sie die Komponentenbibliotheken Ihren Gradle-Dateien hinzufügen.
- Klicken Sie in Android Studio auf den Tab „Projects“ (Projekte) und maximieren Sie den Ordner „Gradle Scripts“ (Gradle-Skripts).
Öffnen Sie build.gradle
(Modul: app).
- Wenden Sie das Kotlin-Plug-in
kapt
annotation processor an, indem Sie es nach den anderen Plug-ins hinzufügen, die oben in der Dateibuild.gradle
(Modul: app) definiert sind.
apply plugin: 'kotlin-kapt'
- Fügen Sie den Block
packagingOptions
in den Blockandroid
ein, um das Modul für atomare Funktionen aus dem Paket auszuschließen und Warnungen zu vermeiden.
android {
// other configuration (buildTypes, defaultConfig, etc.)
packagingOptions {
exclude 'META-INF/atomicfu.kotlin_module'
}
}
- Fügen Sie am Ende des
dependencies
-Blocks den folgenden Code 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"
- Fügen Sie in der Datei
build.gradle
(Project: RoomWordsSample) die Versionsnummern am Ende der Datei hinzu, wie im folgenden Code angegeben.
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 App sind Wörter. Sie benötigen also eine einfache Tabelle, in der diese Werte gespeichert werden:
Mit Room können Sie Tabellen über eine Entität erstellen. Legen wir los.
- Erstellen Sie eine neue Kotlin-Klassendatei mit dem Namen
Word
, die dieWord
-Dataclass enthält.
Diese Klasse beschreibt die Entität (die die SQLite-Tabelle darstellt) für Ihre Wörter. Jede Property in der Klasse stellt eine Spalte in der Tabelle dar. Room verwendet diese Eigenschaften letztendlich, um sowohl die Tabelle zu erstellen als auch 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 Room-Datenbank sinnvoll ist, müssen Sie sie annotieren. Annotationen geben an, wie sich die einzelnen Teile dieser Klasse auf einen Eintrag in der Datenbank beziehen. Room verwendet diese Informationen zum Generieren von Code.
Wenn Sie die Annotationen selbst eingeben (anstatt sie einzufügen), importiert Android Studio die Annotationsklassen automatisch.
- Aktualisieren Sie Ihre
Word
-Klasse mit Anmerkungen, wie in diesem Code gezeigt:
@Entity(tableName = "word_table")
class Word(@PrimaryKey @ColumnInfo(name = "word") val word: String)
Sehen wir uns an, was diese Annotationen bewirken:
@Entity(tableName =
"word_table"
)
Jede@Entity
-Klasse stellt eine SQLite-Tabelle dar. Markieren Sie Ihre Klassendeklaration, um anzugeben, dass es sich um eine Entität handelt. Sie können den Namen der Tabelle angeben, wenn er sich vom Namen der Klasse unterscheiden soll. Dadurch wird die Tabelle „word_table“ genannt.@PrimaryKey
Jede Entität benötigt einen Primärschlüssel. Der Einfachheit halber fungiert jedes Wort als eigener Primärschlüssel.@ColumnInfo(name =
"word"
)
Gibt den Namen der Spalte in der Tabelle an, wenn er sich vom Namen der Mitgliedsvariable unterscheiden soll. Dadurch wird die Spalte „word“ benannt.- Jede in der Datenbank gespeicherte Eigenschaft muss öffentlich sichtbar sein. Das ist der Kotlin-Standard.
Eine vollständige Liste der Annotationen finden Sie in der Referenz zur Zusammenfassung des Raum-Pakets.
Was ist die DAO?
Im DAO (Data Access Object) geben Sie SQL-Abfragen an und verknüpfen sie mit Methodenaufrufen. Der Compiler prüft das SQL und generiert Abfragen aus Convenience-Anmerkungen für häufige Abfragen wie @Insert
. Room verwendet das DAO, um eine übersichtliche API für Ihren Code zu erstellen.
Das DAO muss eine Schnittstelle oder abstrakte Klasse sein.
Standardmäßig müssen alle Abfragen in einem separaten Thread ausgeführt werden.
Room unterstützt Coroutinen. Daher können Ihre Abfragen mit dem Modifikator suspend
annotiert und dann aus einer Coroutine oder einer anderen Unterbrechungsfunktion aufgerufen werden.
DAO implementieren
Wir schreiben ein DAO, das Abfragen für Folgendes bereitstellt:
- Alle Wörter in alphabetischer Reihenfolge abrufen
- Wort einfügen
- Alle Wörter löschen
- Erstellen Sie eine neue Kotlin-Klassendatei mit dem Namen
WordDao
. - Kopieren Sie den folgenden Code in
WordDao
und korrigieren Sie die Importe nach Bedarf, damit der Code kompiliert werden kann.
@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()
}
So gehts:
WordDao
ist eine Schnittstelle. DAOs müssen entweder Schnittstellen oder abstrakte Klassen sein.- Die Annotation
@Dao
kennzeichnet sie als DAO-Klasse für Room. suspend fun insert(word: Word)
: Deklariert eine Suspend-Funktion zum Einfügen eines Worts.- Die
@Insert
-Annotation ist eine spezielle DAO-Methodenannotation, für die Sie keinen SQL-Code angeben müssen. Es gibt auch die Anmerkungen@Delete
und@Update
zum Löschen und Aktualisieren von Zeilen, die Sie in dieser App jedoch nicht verwenden. onConflict = OnConflictStrategy.IGNORE
: Bei der ausgewählten „onConflict“-Strategie wird ein neues Wort ignoriert, wenn es genau mit einem Wort in der Liste übereinstimmt. Weitere Informationen zu den verfügbaren Konfliktstrategien finden Sie in der Dokumentation.suspend fun deleteAll()
: Deklariert eine angehaltene Funktion zum Löschen aller Wörter.- Es gibt keine Convenience-Annotation zum Löschen mehrerer Entitäten. Daher wird die generische Annotation
@Query
verwendet. @Query
("DELETE FROM word_table")
: Für@Query
müssen Sie eine SQL-Abfrage als Stringparameter für die Anmerkung angeben, was komplexe Leseabfragen und andere Vorgänge ermöglicht.fun getAlphabetizedWords(): List<Word>
: Eine Methode, um alle Wörter abzurufen und einList
vonWords
zurückzugeben.@Query(
"SELECT * from word_table ORDER BY word ASC"
)
: Eine Abfrage, die eine Liste von Wörtern in aufsteigender Reihenfolge sortiert zurückgibt.
Wenn sich Daten ändern, möchten Sie in der Regel eine Aktion ausführen, z. B. die aktualisierten Daten in der Benutzeroberfläche anzeigen. Sie müssen die Daten also im Blick behalten, um bei Änderungen reagieren zu können.
Je nachdem, wie die Daten gespeichert werden, kann das schwierig sein. Wenn Sie Änderungen an Daten in mehreren Komponenten Ihrer App beobachten, können explizite, starre Abhängigkeitspfade zwischen den Komponenten entstehen. Das erschwert unter anderem das Testen und Beheben von Fehlern.
LiveData
, eine Lifecycle-Bibliotheksklasse für die Datenbeobachtung, löst dieses Problem. Verwenden Sie in Ihrer Methodenbeschreibung einen Rückgabewert vom Typ LiveData
. Room generiert dann den gesamten erforderlichen Code, um LiveData
zu aktualisieren, wenn die Datenbank aktualisiert wird.
Ändern Sie in WordDao
die Methodensignatur getAlphabetizedWords()
so, dass der 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 verfolgen Sie Datenänderungen über ein Observer
in MainActivity
.
Was ist eine Room-Datenbank?
- Room ist eine Datenbankebene über einer SQLite-Datenbank.
- Room übernimmt alltägliche Aufgaben, die Sie früher mit einem
SQLiteOpenHelper
erledigt haben. - Room verwendet das DAO, um Abfragen an die Datenbank zu senden.
- Um eine schlechte UI-Leistung zu vermeiden, können Sie mit Room standardmäßig keine Abfragen im Hauptthread ausführen. Wenn bei Room-Abfragen
LiveData
zurückgegeben wird, werden die Abfragen automatisch asynchron in einem Hintergrundthread ausgeführt. - Room bietet Kompilierzeitprüfungen von SQLite-Anweisungen.
Room-Datenbank implementieren
Ihre Room-Datenbankklasse muss abstrakt sein und RoomDatabase
erweitern. Normalerweise benötigen Sie nur eine Instanz einer Room-Datenbank für die gesamte App.
Lass uns jetzt einen erstellen.
- Erstellen Sie eine Kotlin-Klassendatei mit dem Namen
WordRoomDatabase
und fügen Sie ihr den 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
}
}
}
}
Sehen wir uns den Code an:
- Die Datenbankklasse für Room muss
abstract
sein undRoomDatabase
erweitern. - Sie annotieren die Klasse als Room-Datenbank mit
@Database
und deklarieren mit den Annotationsparametern die Entitäten, die zur Datenbank gehören, und legen die Versionsnummer fest. Jede Entität entspricht einer Tabelle, die in der Datenbank erstellt wird. Datenbankmigrationen werden in diesem Codelab nicht behandelt. Daher setzen wirexportSchema
hier auf „false“, um eine Build-Warnung zu vermeiden. In einer echten App sollten Sie ein Verzeichnis für Room festlegen, in das das Schema exportiert werden soll, damit Sie das aktuelle Schema in Ihr Versionskontrollsystem einchecken können. - Die Datenbank stellt DAOs über eine abstrakte „Getter“-Methode für jedes @Dao bereit.
- Wir haben ein Singleton,
WordRoomDatabase,
, definiert, um zu verhindern, dass mehrere Instanzen der Datenbank gleichzeitig geöffnet werden. getDatabase
gibt das Singleton zurück. Die Datenbank wird beim ersten Zugriff erstellt. Dazu wird der Datenbank-Builder von Room verwendet, um im Anwendungskontext aus der KlasseWordRoomDatabase
einRoomDatabase
-Objekt zu erstellen und es"word_database"
zu nennen.
Was ist ein Repository?
Eine Repository-Klasse abstrahiert den Zugriff auf mehrere Datenquellen. Das Repository ist nicht Teil der Architecture Components-Bibliotheken, wird aber als Best Practice für die Trennung von Code und Architektur empfohlen. Eine Repository-Klasse bietet eine übersichtliche API für den Datenzugriff für den Rest der Anwendung.
Warum ein Repository verwenden?
Ein Repository verwaltet Abfragen und ermöglicht die Verwendung mehrerer Back-Ends. Im häufigsten Beispiel implementiert das Repository die Logik, um zu entscheiden, ob Daten aus einem Netzwerk abgerufen oder in einer lokalen Datenbank zwischengespeicherte Ergebnisse verwendet werden sollen.
Repository implementieren
Erstellen Sie eine Kotlin-Klassendatei mit dem Namen WordRepository
und fügen Sie den folgenden Code 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)
}
}
Die wichtigsten Erkenntnisse:
- Das DAO wird an den Repository-Konstruktor übergeben, nicht die gesamte Datenbank. Das liegt daran, dass nur Zugriff auf das DAO erforderlich ist, da es alle Lese-/Schreibmethoden für die Datenbank enthält. Sie müssen nicht die gesamte Datenbank für das Repository freigeben.
- Die Liste der Wörter ist eine öffentliche Eigenschaft. Sie wird initialisiert, indem die
LiveData
-Liste der Wörter aus Room abgerufen wird. Das ist möglich, weil wir diegetAlphabetizedWords
-Methode im Schritt „Die LiveData-Klasse“ so definiert haben, dass sieLiveData
zurückgibt. Room führt alle Abfragen in einem separaten Thread aus. Der beobachteteLiveData
benachrichtigt den Beobachter dann im Haupt-Thread, wenn sich die Daten geändert haben. - Der Modifikator
suspend
weist den Compiler darauf hin, dass diese Funktion von einer Coroutine oder einer anderen unterbrechbaren Funktion aufgerufen werden muss.
Was ist ein ViewModel?
Die Rolle von ViewModel
besteht darin, Daten für die Benutzeroberfläche bereitzustellen und Konfigurationsänderungen zu überstehen. Ein ViewModel
fungiert als Kommunikationszentrum zwischen dem Repository und der Benutzeroberfläche. Sie können auch ein ViewModel
verwenden, um Daten zwischen Fragmenten zu teilen. Das ViewModel ist Teil der Lifecycle-Bibliothek.
Eine Einführung in dieses Thema finden Sie unter ViewModel Overview
oder im Blogpost ViewModels: A Simple Example.
Warum ein ViewModel verwenden?
Ein ViewModel
enthält die UI-Daten Ihrer App auf eine Weise, die den Lebenszyklus berücksichtigt und Konfigurationsänderungen überdauert. Wenn Sie die UI-Daten Ihrer App von Ihren Activity
- und Fragment
-Klassen trennen, können Sie das Prinzip der Einzelverantwortung besser einhalten: Ihre Aktivitäten und Fragmente sind für die Darstellung von Daten auf dem Bildschirm verantwortlich, während Ihre ViewModel
alle für die UI erforderlichen Daten enthalten und verarbeiten kann.
Verwenden Sie in der ViewModel
LiveData
für veränderliche Daten, die von der Benutzeroberfläche verwendet oder angezeigt werden. Die Verwendung von LiveData
bietet mehrere Vorteile:
- Sie können die Daten beobachten (anstatt Änderungen abzufragen) und die
Benutzeroberfläche nur aktualisieren, wenn sich die Daten tatsächlich ändern. - Das Repository und die Benutzeroberfläche sind durch die
ViewModel
vollständig getrennt. - Es gibt keine Datenbankaufrufe von
ViewModel
(das wird alles im Repository erledigt), wodurch der Code besser testbar ist.
viewModelScope
In Kotlin werden alle Coroutinen in einem CoroutineScope
ausgeführt. Ein Bereich steuert die Lebensdauer von Coroutinen über seinen Job. Wenn Sie den Job eines Bereichs abbrechen, werden alle in diesem Bereich gestarteten Coroutinen abgebrochen.
Die AndroidX-Bibliothek lifecycle-viewmodel-ktx
fügt viewModelScope
als Erweiterungsfunktion der Klasse ViewModel
hinzu, sodass Sie mit Bereichen arbeiten können.
Weitere Informationen zur Verwendung von Coroutinen im ViewModel finden Sie in Schritt 5 des Codelabs Kotlin-Coroutinen in Android-Apps verwenden oder im Blogpost „Easy Coroutines in Android: viewModelScope“.
ViewModel implementieren
Erstellen Sie eine Kotlin-Klassendatei für WordViewModel
und fügen Sie ihr diesen 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)
}
}
Hier haben wir:
- Es wurde eine Klasse mit dem Namen
WordViewModel
erstellt, dieApplication
als Parameter erhält undAndroidViewModel
erweitert. - Es wurde eine private Mitgliedsvariable hinzugefügt, um einen Verweis auf das Repository zu speichern.
- Eine öffentliche
LiveData
-Member-Variable zum Zwischenspeichern der Wortliste wurde hinzugefügt. - Es wurde ein
init
-Block erstellt, der einen Verweis aufWordDao
ausWordRoomDatabase
abruft. - Im Block
init
wurdeWordRepository
auf Grundlage vonWordRoomDatabase
erstellt. - Im
init
-Block wird dieallWords
-LiveData mit dem Repository initialisiert. - Es wurde eine Wrapper-Methode
insert()
erstellt, die die Methodeinsert()
des Repositorys aufruft. So wird die Implementierung voninsert()
von der Benutzeroberfläche gekapselt. Wir möchten nicht, dass das Einfügen den Hauptthread blockiert. Daher starten wir eine neue Coroutine und rufen die Einfügefunktion des Repositorys auf, die eine suspend-Funktion ist. Wie bereits erwähnt, haben ViewModels einen Coroutine-Scope, der auf ihrem Lebenszyklus basiert undviewModelScope
heißt. Wir verwenden ihn hier.
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. Wir stellen Ihnen daher nur den Code zur Verfügung.
Legen Sie das Design Ihrer Anwendung auf „Material“ fest, indem Sie das AppTheme
-Element auf Theme.MaterialComponents.Light.DarkActionBar
festlegen. Fügen Sie in values/styles.xml
einen Stil für Listenelemente 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>
So 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
das TextView
durch ein RecyclerView
und fügen Sie eine unverankerte Aktionsschaltfläche (UAS) 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>
Das Aussehen des schwebenden Aktionsschaltfläche sollte der verfügbaren Aktion entsprechen. Daher muss das Symbol durch ein „+“-Symbol ersetzt werden.
Zuerst müssen wir ein neues Vektor-Asset hinzufügen:
- Wählen Sie Datei > Neu > Vektor-Asset aus.
- Klicken Sie im Feld Clip Art auf das Android-Robotersymbol.
- Suchen Sie nach „add“ und wählen Sie das Asset „+“ aus. Klicken Sie auf OK
.
- Klicken Sie anschließend auf Weiter.
- Bestätigen Sie den Symbolpfad als
main > drawable
und klicken Sie auf Fertig stellen, um das Asset hinzuzufügen. - Aktualisieren Sie in
layout/activity_main.xml
das FAB, damit es das neue Drawable enthält:
<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"/>
Sie möchten die Daten in einem RecyclerView
darstellen, was etwas ansprechender ist, als sie einfach in ein TextView
zu schreiben. In diesem Codelab wird davon ausgegangen, dass Sie wissen, wie RecyclerView
, RecyclerView.LayoutManager
, RecyclerView.ViewHolder
und RecyclerView.Adapter
funktionieren.
Die Daten werden in der Variablen words
im Adapter zwischengespeichert. Im nächsten Schritt fügen Sie den Code hinzu, mit dem die Daten automatisch aktualisiert werden.
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ügen Sie RecyclerView
in die Methode onCreate()
von MainActivity
ein.
In der Methode onCreate()
nach setContentView
:
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 zu prüfen, ob alles funktioniert. Es sind keine Elemente vorhanden, da Sie die Daten noch nicht verknüpft haben.
Die Datenbank enthält keine Daten. Sie fügen Daten auf zwei Arten hinzu: Sie fügen einige Daten hinzu, wenn die Datenbank geöffnet wird, und Sie fügen ein Activity
zum Hinzufügen von Wörtern hinzu.
Wenn Sie alle Inhalte löschen und die Datenbank bei jedem Start der App neu befüllen möchten, erstellen Sie ein RoomDatabase.Callback
und überschreiben onOpen()
. Da Sie keine Room-Datenbankvorgänge im UI-Thread ausführen können, startet onOpen()
eine Coroutine im IO-Dispatcher.
Zum Starten einer Coroutine benötigen wir einen CoroutineScope
. Aktualisieren Sie die Methode getDatabase
der Klasse WordRoomDatabase
, um auch einen Coroutine-Bereich als Parameter zu erhalten:
fun getDatabase(
context: Context,
scope: CoroutineScope
): WordRoomDatabase {
...
}
Aktualisieren Sie die Initialisierung des Datenbankabrufs im Block init
von WordViewModel
, um auch den Bereich zu übergeben:
val wordsDao = WordRoomDatabase.getDatabase(application, viewModelScope).wordDao()
Im WordRoomDatabase
erstellen wir eine benutzerdefinierte Implementierung des RoomDatabase.Callback()
, das auch ein CoroutineScope
als Konstruktorparameter erhält. Anschließend überschreiben wir die Methode onOpen
, um die Datenbank zu füllen.
Hier ist der Code zum Erstellen des Callbacks innerhalb 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 in die Datenbankerstellungssequenz ein, direkt bevor Sie .build()
für die Room.databaseBuilder()
aufrufen:
.addCallback(WordDatabaseCallback(scope))
So sollte der endgültige Code 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>
Fügen Sie diese Farbressource in value/colors.xml
hinzu:
<color name="buttonLabel">#FFFFFF</color>
So erstellen Sie eine neue Dimensionsressourcendatei:
- Klicken Sie im Fenster Projekt auf das App-Modul.
- Wählen Sie Datei > Neu > Android-Ressourcendatei aus.
- Wählen Sie unter „Verfügbare Qualifizierer“ die Option Dimension aus.
- Legen Sie den Dateinamen fest: „dimens“.
Fügen Sie diese Dimensionsressourcen in values/dimens.xml
hinzu:
<dimen name="small_padding">8dp</dimen>
<dimen name="big_padding">16dp</dimen>
Erstellen Sie ein neues leeres Android-Activity
mit der Vorlage „Empty Activity“:
- Wählen Sie Datei > Neu > Aktivität > Leere Aktivität aus.
- Geben Sie
NewWordActivity
für den Aktivitätsnamen ein. - Prüfen Sie, ob die neue Aktivität dem Android-Manifest hinzugefügt wurde.
<activity android:name=".NewWordActivity"></activity>
Aktualisieren Sie 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, die der Nutzer eingibt, gespeichert und der aktuelle Inhalt der Wortdatenbank in der RecyclerView
angezeigt wird.
Wenn Sie den aktuellen Inhalt der Datenbank anzeigen möchten, fügen Sie einen Observer hinzu, der LiveData
in ViewModel
beobachtet.
Immer wenn sich die Daten ändern, wird der onChanged()
-Callback aufgerufen. Dadurch wird die setWords()
-Methode des Adapters aufgerufen, um die im Cache gespeicherten Daten des Adapters zu aktualisieren und die angezeigte Liste zu aktualisieren.
Erstellen Sie in MainActivity
eine Mitgliedsvariable für ViewModel
:
private lateinit var wordViewModel: WordViewModel
Verwenden Sie ViewModelProvider
, um Ihre ViewModel
mit Ihrem Activity
zu verknüpfen.
Wenn Activity
zum ersten Mal gestartet wird, erstellt ViewModelProviders
die ViewModel
. Wenn die Aktivität zerstört wird, z. B. durch eine Konfigurationsänderung, bleibt ViewModel
erhalten. Wenn die Aktivität neu erstellt wird, gibt ViewModelProviders
die vorhandene ViewModel
zurück. Weitere Informationen finden Sie unter ViewModel
.
Rufen Sie in onCreate()
unter dem Codeblock RecyclerView
eine ViewModel
aus der ViewModelProvider
ab:
wordViewModel = ViewModelProvider(this).get(WordViewModel::class.java)
Fügen Sie in onCreate()
außerdem einen Observer für das Attribut „allWords“ LiveData
aus WordViewModel
.
hinzu.
Die Methode onChanged()
(die Standardmethode für unsere Lambda-Funktion) wird ausgelöst, wenn sich die beobachteten Daten ändern und die Aktivität im Vordergrund ist:
wordViewModel.allWords.observe(this, Observer { words ->
// Update the cached copy of the words in the adapter.
words?.let { adapter.setWords(it) }
})
Wenn wir auf das FAB tippen, soll NewWordActivity
geöffnet werden. Wenn wir dann wieder in MainActivity
sind, soll das neue Wort entweder in die Datenbank eingefügt oder eine Toast
angezeigt werden. Dazu definieren wir zuerst einen Anforderungscode:
private val newWordActivityRequestCode = 1
Fügen Sie in MainActivity
den onActivityResult()
-Code für NewWordActivity
ein.
Wenn die Aktivität mit RESULT_OK
zurückgegeben wird, fügen Sie das zurückgegebene Wort in die Datenbank ein, indem Sie die Methode insert()
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()
}
}
In MainActivity,
start NewWordActivity
, wenn der Nutzer auf die Schaltfläche tippt. Suchen Sie im MainActivity
onCreate
nach dem FAB und fügen Sie ein 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 App aus. Wenn Sie der Datenbank in NewWordActivity
ein Wort hinzufügen, wird die Benutzeroberfläche automatisch aktualisiert.
Nachdem Sie nun eine funktionierende App haben, fassen wir noch einmal zusammen, was Sie erstellt haben. Hier noch einmal die App-Struktur:
Die Komponenten der App sind:
MainActivity
: Zeigt Wörter in einer Liste mit einemRecyclerView
und demWordListAdapter
an. InMainActivity
gibt es einObserver
, das die LiveData aus der Datenbank beobachtet und benachrichtigt wird, wenn sie sich ändern.- Mit
NewWordActivity:
wird der Liste ein neues Wort hinzugefügt. WordViewModel
: bietet Methoden für den Zugriff auf die Datenschicht und gibt LiveData zurück, damit MainActivity die Observer-Beziehung einrichten kann.*LiveData<List<Word>>
: Ermöglicht automatische Updates in den UI-Komponenten. ImMainActivity
gibt es einObserver
, das die LiveData aus der Datenbank beobachtet und benachrichtigt wird, wenn sie sich ändern.Repository:
verwaltet eine oder mehrere Datenquellen. DieRepository
stellt Methoden bereit, mit denen das ViewModel mit dem zugrunde liegenden Datenanbieter interagieren kann. In dieser App ist das Backend eine Room-Datenbank.Room
: ist ein Wrapper für eine SQLite-Datenbank und implementiert diese. Room erledigt viel Arbeit für Sie, die Sie früher selbst erledigen mussten.- DAO: Ordnet Methodenaufrufe Datenbankabfragen zu. Wenn das Repository also eine Methode wie
getAlphabetizedWords()
aufruft, kann RoomSELECT * from word_table ORDER BY word ASC
ausführen. Word
: die Entitätsklasse, die ein einzelnes Wort enthält.
* Views
und Activities
(und Fragments
) interagieren nur über die ViewModel
mit den Daten. Es spielt also keine Rolle, woher die Daten stammen.
Datenfluss für automatische UI-Updates (reaktive UI)
Das automatische Update ist möglich, weil wir LiveData verwenden. Im MainActivity
gibt es ein Observer
, das die LiveData aus der Datenbank beobachtet und benachrichtigt wird, wenn sie sich ändern. Bei einer Änderung wird die onChange()
-Methode des Beobachters ausgeführt und mWords
im WordListAdapter
aktualisiert.
Die Daten sind sichtbar, weil sie LiveData
sind. Beobachtet wird die LiveData<List<Word>>
, die von der WordViewModel
-Property allWords
zurückgegeben wird.
Mit WordViewModel
wird alles über das Backend in der UI-Ebene ausgeblendet. Sie bietet Methoden für den Zugriff auf die Datenschicht und gibt LiveData
zurück, damit MainActivity
die Observer-Beziehung einrichten kann. Views
und Activities
(und Fragments
) interagieren nur über die ViewModel
mit den Daten. Es spielt also keine Rolle, woher die Daten stammen.
In diesem Fall stammen die Daten aus einem Repository
. Die ViewModel
muss nicht wissen, womit das Repository interagiert. Es muss nur wissen, wie es mit dem Repository
interagieren kann, und zwar über die vom Repository
bereitgestellten Methoden.
Das Repository verwaltet eine oder mehrere Datenquellen. In der WordListSample
App ist dieses Backend eine Room-Datenbank. Room ist ein Wrapper für eine SQLite-Datenbank und implementiert diese. Room erledigt viel Arbeit für Sie, die Sie früher selbst erledigen mussten. Room bietet beispielsweise alle Funktionen, die Sie bisher in einer SQLiteOpenHelper
-Klasse genutzt haben.
Das DAO ordnet Methodenaufrufe Datenbankabfragen zu. Wenn das Repository also eine Methode wie getAllWords()
aufruft, kann Room SELECT * from word_table ORDER BY word ASC
ausführen.
Da das von der Abfrage zurückgegebene Ergebnis beobachtet wird (LiveData
), wird jedes Mal, wenn sich die Daten in Room ändern, die onChanged()
-Methode der Observer
-Schnittstelle ausgeführt und die Benutzeroberfläche aktualisiert.
[Optional] Lösungscode herunterladen
Falls noch nicht geschehen, 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:
Entpacken Sie die heruntergeladene ZIP-Datei. Dadurch wird ein Stammordner (android-room-with-a-view-kotlin
) entpackt, der die vollständige App enthält.