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:
RecyclerView
und Adapter- SQLite-Datenbank und SQLite-Abfragesprache
- Allgemeine Koroutinen (wenn Sie mit Coroutinen nicht vertraut sind, können Sie Kotlin-Koroutinen in Android-Apps verwenden verwenden)
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
aufMainActivity
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.
Was sind die empfohlenen Architekturkomponenten?
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.
- Öffnen Sie Android Studio und klicken Sie auf Start a new Android Studio project.
- Wählen Sie im Fenster „Neues Projekt erstellen“ die Option Leere Aktivität aus und klicken Sie auf Weiter.
- 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.
- Klicken Sie in Android Studio auf den Tab „Projekte“ und maximieren Sie den Ordner „Gradle Scripts“.
Öffne build.gradle
(Modul: app).
- 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 Dateibuild.gradle
(Modul: App) definiert wurden.
apply plugin: 'kotlin-kapt'
- Fügen Sie den Block
packagingOptions
in den Blockandroid
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'
}
}
- 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"
- 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.
- Erstelle eine neue Kotlin-Klassendatei mit dem Namen
Word
, die die DatenklasseWord
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.
- 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
- Erstellen Sie eine neue Kotlin-Klassendatei mit dem Namen
WordDao
. - 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 undList
vonWords
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.
- 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 undRoomDatabase
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 hierexportSchema
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 Singleton
WordRoomDatabase,
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 einRoomDatabase
Objekt im Anwendungskontext aus derWordRoomDatabase
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 diegetAlphabetizedWords
-Methode festgelegt wurde, dassLiveData
im Schritt „LiveData“ zurückgegeben werden soll. Der Chatroom führt alle Abfragen in einem separaten Thread aus. Über den beobachteten ZustandLiveData
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, dieApplication
als Parameter erhält undAndroidViewModel
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 denWordDao
aus derWordRoomDatabase
erhält. - Im
init
-Block wurde derWordRepository
basierend auf demWordRoomDatabase
erstellt. - Im
init
-Block wurden dieallWords
-LiveData mithilfe des Repositorys initialisiert. - Es wurde eine Wrapper-
insert()
-Methode erstellt, die die Repository-Methodeinsert()
aufruft. So wird die Implementierung voninsert()
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 LebenszyklusviewModelScope
einen 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.
- Wählen Sie Datei > Neues > Vektor-Asset aus.
- Klicken Sie im Feld Clip Art: auf das Android-Robot-Symbol.
- Suche nach „Add“ und wähle 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 Finish (Fertig), um das Asset hinzuzufügen. - 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:
- Klicken Sie im Fenster Projekt auf das App-Modul.
- Wähle File > New > Android Resource File aus
- Wählen Sie unter „Verfügbare Kennzeichner“ die Option Dimension aus.
- 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
:
- Wählen Sie Datei > Aktivität > Leere Aktivität aus.
- Geben Sie als Aktivitätsnamen
NewWordActivity
ein. - Ü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 mitRecyclerView
undWordListAdapter
an InMainActivity
gibt es einenObserver
, 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 ImMainActivity
ist einObserver
enthalten, der die Wörter „LiveData“ aus der Datenbank beobachtet und über ihre Änderungen benachrichtigt wird.Repository:
verwaltet eine oder mehrere Datenquellen. UnterRepository
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 RoomSELECT * 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:
Entpacken Sie die heruntergeladene ZIP-Datei. Dadurch wird der Stammordner android-room-with-a-view-kotlin
entpackt, der die vollständige Anwendung enthält.