Die UI-Ebene enthält UI-bezogene Status und UI-Logik, während die Datenschicht Anwendungsdaten und Geschäftslogik enthält. Die Geschäftslogik ist das, gibt Ihrer App einen Mehrwert – sie besteht aus realen Geschäftsregeln, die bestimmen, wie Anwendungsdaten erstellt, gespeichert und geändert werden müssen.
Durch die Trennung der Daten kann die Datenschicht auf mehreren Ebenen Bildschirmen zu teilen, Informationen zwischen verschiedenen Bereichen der App auszutauschen außerhalb der Benutzeroberfläche für Unittests testen. Weitere Informationen zu Informationen zu den Vorteilen der Datenschicht finden Sie in der Architekturübersicht. .
Architektur der Datenschicht
Die Datenschicht besteht aus Repositories, die jeweils null bis viele enthalten können.
Datenquellen. Sie sollten eine Repository-Klasse für jeden Typ von
Daten, die Sie in Ihrer App verarbeiten. Sie können beispielsweise eine MoviesRepository
erstellen.
Klasse für Daten zu Filmen oder eine PaymentsRepository
-Klasse für Daten
im Zusammenhang mit Zahlungen.
![In einer typischen Architektur stellen
die Repositories der Datenschicht Daten bereit,
auf den Rest der App anwenden
und hängen von den Datenquellen ab.](https://developer.android.com/static/topic/libraries/architecture/images/mad-arch-data-overview.png?authuser=9&hl=de)
Repository-Klassen sind für die folgenden Aufgaben verantwortlich:
- Daten für den Rest der Anwendung verfügbar machen
- Änderungen an den Daten zentralisieren.
- Konflikte zwischen mehreren Datenquellen beheben
- Datenquellen vom Rest der App abstrahieren
- Enthält Geschäftslogik.
Jede Datenquellenklasse sollte nur mit jeweils einer Datenquelle, bei der es sich um eine Datei, eine Netzwerkquelle oder eine lokale Datenbank handeln kann. Daten Quellklassen sind die Brücke zwischen Anwendung und System für Daten Geschäftsabläufe.
Andere Ebenen in der Hierarchie sollten niemals direkt auf Datenquellen zugreifen. die Einstiegspunkte für die Datenschicht sind immer die Repository-Klassen. Staatsinhaber (siehe UI-Ebenenleitfaden) oder verwenden Sie Fallklassen (siehe Leitfaden zu Domainebenen) sollten nie eine Datenquelle als direkte Abhängigkeit haben. Repository-Klassen verwenden als Einstiegspunkte ermöglichen die Skalierung der verschiedenen Ebenen der Architektur unabhängig voneinander unterscheiden.
Die von dieser Ebene bereitgestellten Daten sollten unveränderlich sein, damit sie nicht die von anderen Klassen manipuliert werden, ist ein inkonsistenter Zustand. Unveränderliche Daten können auch von mehreren Threads. Weitere Informationen finden Sie im Abschnitt zum Threading.
Gemäß den Best Practices für das Einschleusen von Abhängigkeiten Das Repository behandelt Datenquellen im Konstruktor als Abhängigkeiten:
class ExampleRepository(
private val exampleRemoteDataSource: ExampleRemoteDataSource, // network
private val exampleLocalDataSource: ExampleLocalDataSource // database
) { /* ... */ }
APIs freigeben
Die Klassen in der Datenschicht bieten in der Regel Funktionen zum Erstellen, CRUD-Aufrufe (Read, Update and Delete) oder Benachrichtigungen bei Datenänderungen erhalten . In der Datenschicht sollten für jeden dieser Fälle folgende Informationen angezeigt werden:
- One-Shot-Vorgänge:Die Datenschicht sollte Sperrungsfunktionen in
Kotlin Bei der Programmiersprache Java sollte die Datenschicht
Funktionen, die einen Callback bereitstellen, um über das Ergebnis des Vorgangs zu informieren, oder
RxJava-Typen
Single
,Maybe
oderCompletable
. - Wenn Sie über Datenänderungen im Zeitverlauf benachrichtigt werden möchten:In der Datenschicht sollten
flows in Kotlin und für die Programmiersprache Java
die Datenschicht einen Callback enthalten, der die neuen Daten ausgibt, oder die RxJava-
Observable
- oderFlowable
-Typ.
class ExampleRepository(
private val exampleRemoteDataSource: ExampleRemoteDataSource, // network
private val exampleLocalDataSource: ExampleLocalDataSource // database
) {
val data: Flow<Example> = ...
suspend fun modifyData(example: Example) { ... }
}
Namenskonventionen in diesem Leitfaden
In diesem Leitfaden werden Repository-Klassen nach den Daten benannt, verantwortlich ist. Die Konvention lautet wie folgt:
type of data + Repository:
Beispiel: NewsRepository
, MoviesRepository
oder PaymentsRepository
.
Datenquellenklassen sind nach den Daten benannt, für die sie verantwortlich sind, und dem welche Quelle sie verwenden. Die Konvention lautet wie folgt:
Datentyp + Quelltyp + Datenquelle.
Verwenden Sie als Datentyp Remote oder Local für allgemeinere,
Implementierungen sich ändern können. Beispiel: NewsRemoteDataSource
oder
NewsLocalDataSource
. Wenn die Quelle wichtig ist, verwenden Sie
den Typ der Quelle. Beispiel: NewsNetworkDataSource
oder
NewsDiskDataSource
.
Benennen Sie die Datenquelle nicht basierend auf einem Implementierungsdetail, z. B.
UserSharedPreferencesDataSource
, weil Repositories, die diese Datenquelle verwenden,
nicht wissen, wie die Daten gespeichert werden. Wenn du dich an diese Regel hältst, kannst du
die Implementierung der Datenquelle (z. B. Migration von
SharedPreferences für
DataStore) ohne Auswirkungen auf den
die diese Quelle aufruft.
Mehrere Ebenen von Repositories
Bei komplexeren Geschäftsanforderungen kann ein Repository von anderen Repositories abhängig sein. Das könnte daran liegen, dass die beteiligten Daten Aggregation aus mehreren Datenquellen oder weil die Verantwortung in einer anderen Repository-Klasse gekapselt werden soll.
Ein Repository, das Daten
zur Nutzerauthentifizierung verarbeitet,
UserRepository
, könnte von anderen Repositories wie LoginRepository
abhängen
und RegistrationRepository
, um die Anforderungen zu erfüllen.
![In diesem Beispiel hängt UserRepository von zwei anderen Repository-Klassen ab:
LoginRepository, das von anderen Anmeldedatenquellen abhängt und
RegistrationRepository, das von anderen Registrierungsdatenquellen abhängt.](https://developer.android.com/static/topic/libraries/architecture/images/mad-arch-data-multiple-repos.png?authuser=9&hl=de)
Quelle der Wahrheit
Es ist wichtig, dass jedes Repository eine Single Source of Truth definiert. Die Quelle of Truth enthält immer Daten, die einheitlich, richtig und aktuell sind. In sollten die aus dem Repository preisgegebenen Daten immer direkt aus der "Source of Truth".
Die zentrale Informationsquelle kann eine Datenquelle sein, z. B. die Datenbank, oder sogar speicherinternen Cache, den das Repository enthalten könnte. Repositories kombinieren verschiedene Datenquellen nutzen und potenzielle Konflikte zwischen den Daten die Single Source of Truth regelmäßig oder aufgrund von Nutzereingaben zu aktualisieren. .
Verschiedene Repositories in Ihrer App können unterschiedliche Quellen der Wahrheit haben. Für
Beispiel: Die Klasse LoginRepository
könnte ihren Cache als „Source of Truth“ verwenden
und die Klasse PaymentsRepository
die Netzwerkdatenquelle verwendet.
Um Offline-First-Support zu bieten, muss eine lokale Datenquelle wie „Datenbank“ ist die empfohlene Informationsquelle.
Threading
Datenquellen und Repositories für Aufrufe sollten hauptsicher sein, d. h., sie können sicher von dort aufgerufen werden. im Hauptthread. Diese Klassen sind dafür verantwortlich, Logik zum entsprechenden Thread hinzufügen, wenn das Blockieren mit langer Ausführungszeit erfolgt Geschäftsabläufe. Es sollte beispielsweise sicher sein, dass eine Datenquelle aus einem oder für ein Repository, das aufwendige Filterung in einer großen Liste durchführt.
Beachten Sie, dass die meisten Datenquellen bereits hauptsichere APIs wie die Sperre bereitstellen. von Room bereitgestellte Methodenaufrufe, Retrofit oder Ktor Ihr Repository kann nutzen diese APIs, sobald sie verfügbar sind.
Weitere Informationen zum Threading finden Sie im Leitfaden zum Hintergrund verarbeitet werden. Für Kotlin-Nutzer: coroutines ist die empfohlene Option. Siehe Laufen Android-Aufgaben in Hintergrundthreads für empfohlenen Optionen für die Programmiersprache Java.
Lebenszyklus
Instanzen von Klassen in der Datenschicht verbleiben im Arbeitsspeicher, die über den Stamm der automatischen Speicherbereinigung erreichbar sind – normalerweise durch Verweis von anderen Objekte in Ihrer App an.
Wenn eine Klasse speicherinterne Daten enthält (z. B. einen Cache), möchten Sie diese möglicherweise dieselbe Instanz dieser Klasse für einen bestimmten Zeitraum. Dies ist auch Dies wird als Lebenszyklus der Klasseninstanz bezeichnet.
Wenn die Verantwortlichkeit der Klasse für die gesamte Anwendung entscheidend ist, können Sie
scope ist eine Instanz dieser Klasse in der Application
-Klasse. Das macht es so,
die Instanz dem Lebenszyklus der Anwendung folgt. Wenn Sie nur
dieselbe Instanz in einem bestimmten Ablauf in Ihrer App wiederverwenden, z. B.
Registrierungs- oder Anmeldevorgang abschließen, sollten Sie den Umfang der Instanz auf die Klasse
für den Lebenszyklus
dieses Ablaufs verantwortlich ist. Sie könnten beispielsweise den Umfang einer
RegistrationRepository
, die speicherinterne Daten für den
RegistrationActivity
oder die Navigation
Grafik der
Registrierungsprozess.
Der Lebenszyklus jeder Instanz ist ein entscheidender Faktor bei der Entscheidung, wie App-Abhängigkeiten zu erkennen. Es empfiehlt sich, die Abhängigkeit Best Practices für die Injektion, bei denen die Abhängigkeiten verwaltet werden und können Abhängigkeitscontainern zugeordnet werden. Weitere Informationen über Weitere Informationen zum Festlegen des Gültigkeitsbereichs in Android und Griff Blogpost.
Geschäftsmodelle darstellen
Die Datenmodelle, die Sie aus der Datenschicht zur Verfügung stellen möchten, können eine Teilmenge von die Sie aus den verschiedenen Datenquellen erhalten. Im Idealfall aus verschiedenen Datenquellen – sowohl Netzwerk- als auch lokale – sollten nur die Informationen zurückgeben, die Ihre Anwendung benötigt; aber das ist nicht oft der Fall.
Stellen Sie sich beispielsweise einen News API-Server vor, der nicht nur den Artikel Informationen, sondern auch den Änderungsverlauf, Nutzerkommentare und einige Metadaten:
data class ArticleApiModel(
val id: Long,
val title: String,
val content: String,
val publicationDate: Date,
val modifications: Array<ArticleApiModel>,
val comments: Array<CommentApiModel>,
val lastModificationDate: Date,
val authorId: Long,
val authorName: String,
val authorDateOfBirth: Date,
val readTimeMin: Int
)
Die App benötigt nicht so viele Informationen über den Artikel,
Der Inhalt des Artikels wird zusammen mit grundlegenden Informationen auf dem Bildschirm angezeigt.
über seinen Autor. Es empfiehlt sich, die Modellklassen zu trennen
Repositories stellen nur die Daten bereit, die von den anderen Ebenen der Hierarchie
erfordern. Hier sehen Sie zum Beispiel, wie Sie die ArticleApiModel
von
Netzwerk, um eine Article
-Modellklasse für die Domain und die Benutzeroberfläche verfügbar zu machen
Ebenen:
data class Article(
val id: Long,
val title: String,
val content: String,
val publicationDate: Date,
val authorName: String,
val readTimeMin: Int
)
Das Trennen von Modellklassen ist auf folgende Weise von Vorteil:
- Dadurch wird der App-Arbeitsspeicher gespart, da nur die Daten auf das Nötigste reduziert werden.
- Sie passt externe Datentypen an die von Ihrer App verwendeten Datentypen an, z. B. Ihre App verwendet möglicherweise einen anderen Datentyp für die Darstellung von Datumsangaben.
- Es ermöglicht eine bessere Trennung von Bedenken, z. B. von Mitgliedern eines großen Teams. kann individuell auf den Netzwerk- und UI-Ebenen eines Features funktioniert werden, wenn das Modell Klasse vorab definiert.
Sie können diese Vorgehensweise erweitern und separate Modellklassen in anderen Teilen der Ihrer App-Architektur an, z. B. in Datenquellenklassen und ViewModels ansehen. Dazu müssen Sie jedoch zusätzliche Klassen und Logik definieren, die Sie ordnungsgemäß dokumentieren und testen sollten. Sie sollten mindestens neue Modelle zu erstellen, immer dann, wenn eine Datenquelle Daten empfängt, den Erwartungen der App entsprechen.
Arten von Datenoperationen
In der Datenschicht sind Operationen möglich, die je nachdem, wie wichtig sind sie: UI-orientiert, app-orientiert und geschäftsorientiert.
UI-orientierte Operationen
UI-orientierte Operationen sind nur relevant, wenn sich die Nutzenden auf einem bestimmten Bildschirm befinden, und sie werden abgebrochen, wenn der Nutzer den Bildschirm verlässt. Ein Beispiel: Anzeige einiger Daten aus der Datenbank.
UI-orientierte Operationen werden in der Regel von der UI-Ebene ausgelöst und folgen den Lebenszyklus des Aufrufers, z. B. der Lebenszyklus der ViewModel. Weitere Informationen finden Sie im Abschnitt Abschnitt „Netzwerkanfrage“ ein Beispiel für eine UI-orientierte .
App-orientierte Abläufe
App-orientierte Vorgänge sind relevant, solange die App geöffnet ist. Wenn die App geschlossen oder der Prozess beendet wird, werden diese Vorgänge abgebrochen. Ein Beispiel: Das Ergebnis einer Netzwerkanfrage wird im Cache gespeichert, damit es bei Bedarf später verwendet werden kann. Weitere Informationen finden Sie im Abschnitt In-Memory-Daten-Caching implementieren. mehr.
Diese Vorgänge folgen normalerweise dem Lebenszyklus der Application
-Klasse oder
mit der Datenschicht. Ein Beispiel finden Sie im Abschnitt Einen Vorgang länger als die
Bildschirm angezeigt.
Geschäftsorientierte Abläufe
Geschäftsorientierte Vorgänge können nicht abgebrochen werden. Sie sollten den Prozess überleben Tod. Ein Beispiel ist der Abschluss des Uploads eines Fotos, das der Nutzer posten möchte zu seinem Profil hinzufügen.
Die Empfehlung für geschäftsorientierte Abläufe ist die Verwendung von WorkManager. Weitere Informationen finden Sie unter im Abschnitt Aufgaben mit WorkManager planen.
Fehler freigeben
Interaktionen mit Repositories und Datenquellen können entweder erfolgreich sein oder
wenn ein Fehler auftritt. Für Koroutinen und Abläufe sollten Sie Folgendes verwenden:
Integrierte Fehlerbehandlung in Kotlin
Mechanismus. Für
Fehler, die durch Sperrungsfunktionen ausgelöst werden können, sollten Sie try/catch
-Blöcke verwenden, wenn
angemessen; und in Abläufen können Sie
catch
. Bei diesem Ansatz wird erwartet, dass die UI-Ebene Ausnahmen verarbeitet, wenn
die Datenschicht aufrufen.
Die Datenschicht kann verschiedene
Fehlertypen verstehen und verarbeiten
mithilfe von benutzerdefinierten Ausnahmen, z. B. UserNotAuthenticatedException
.
Weitere Informationen zu Fehlern in Koroutinen finden Sie unter Ausnahmen in Koroutinen Blogpost.
Allgemeine Aufgaben
In den folgenden Abschnitten finden Sie Beispiele für die Verwendung und Architektur der Daten. zur Ausführung bestimmter in Android-Apps üblicher Aufgaben. Beispiele: basierend auf der typischen Nachrichten-App, die zuvor in diesem Leitfaden erwähnt wurde.
Netzwerkanfrage senden
Eine Netzwerkanfrage gehört zu den häufigsten Aufgaben,
zu erzielen. Die Nachrichten-App muss dem Nutzer aktuelle Nachrichten
die aus dem Netzwerk abgerufen werden. Daher benötigt die Anwendung eine Datenquellenklasse, um
Netzwerkbetrieb: NewsRemoteDataSource
. Um die Informationen für die
ist ein neues Repository,
das Abläufe zu Nachrichtendaten abwickelt,
Erstellt: NewsRepository
.
Die Voraussetzung ist, dass die Nachrichten immer auf dem neuesten Stand sind, wenn Nutzer öffnet den Bildschirm. Daher ist dies ein UI-orientierter Vorgang.
Datenquelle erstellen
Die Datenquelle muss eine Funktion verfügbar machen, die die neuesten Nachrichten zurückgibt: eine Liste
von ArticleHeadline
Instanzen. Die Datenquelle muss eine wesentlich sichere Methode bieten,
um aktuelle Nachrichten vom Netzwerk zu erhalten. Dafür ist eine
Abhängigkeit von CoroutineDispatcher
oder Executor
, auf dem die Aufgabe ausgeführt werden soll.
Eine Netzwerkanfrage ist ein One-Shot-Aufruf, der von einem neuen fetchLatestNews()
verarbeitet wird
:
class NewsRemoteDataSource(
private val newsApi: NewsApi,
private val ioDispatcher: CoroutineDispatcher
) {
/**
* Fetches the latest news from the network and returns the result.
* This executes on an IO-optimized thread pool, the function is main-safe.
*/
suspend fun fetchLatestNews(): List<ArticleHeadline> =
// Move the execution to an IO-optimized thread since the ApiService
// doesn't support coroutines and makes synchronous requests.
withContext(ioDispatcher) {
newsApi.fetchLatestNews()
}
}
// Makes news-related network synchronous requests.
interface NewsApi {
fun fetchLatestNews(): List<ArticleHeadline>
}
Die NewsApi
-Schnittstelle verbirgt die Implementierung des Netzwerk-API-Clients. sie
keinen Einfluss darauf, ob die Schnittstelle durch
Retrofit oder
HttpURLConnection
Zuverlässig
Schnittstellen machen API-Implementierungen in Ihrer App anpassbar.
Repository erstellen
Da in der Repository-Klasse
für diese Aufgabe keine zusätzliche Logik erforderlich ist,
NewsRepository
fungiert als Proxy für die Netzwerkdatenquelle. Die Vorteile von
zusätzliche Abstraktionsebene hinzugefügt, werden im In-Memory-
Caching.
// NewsRepository is consumed from other layers of the hierarchy.
class NewsRepository(
private val newsRemoteDataSource: NewsRemoteDataSource
) {
suspend fun fetchLatestNews(): List<ArticleHeadline> =
newsRemoteDataSource.fetchLatestNews()
}
Wie Sie die Repository-Klasse direkt über die UI-Ebene nutzen, erfahren Sie in der Leitfaden für UI-Ebenen
Speicherinternes Daten-Caching implementieren
Angenommen, eine neue Anforderung für die Nachrichten-App wird eingeführt: wenn der Nutzer die App öffnet. angezeigt werden, müssen dem Nutzer im Cache gespeicherte Nachrichten angezeigt werden, wenn eine Anfrage gesendet wurde. . Andernfalls sollte die App eine Netzwerkanfrage senden, um die neueste Nachrichten.
Aufgrund der neuen Anforderung muss die App die neuesten Nachrichten im Arbeitsspeicher speichern, während der Nutzer die App geöffnet hat. Es handelt sich also um einen app-orientierten Vorgang.
Caches
Sie können Daten sichern, während sich der Nutzer in Ihrer App befindet, indem Sie speicherinterne Daten hinzufügen Caching. Caches sind dazu gedacht, Informationen für eine bestimmte zu verwenden – in diesem Fall solange, wie sich der Nutzer in der App befindet. Zwischenspeicher Implementierungen können unterschiedliche Formen annehmen. Sie kann von einfachen änderbaren Variable zu einer komplexeren Klasse, die vor Lese-/Schreibvorgängen schützt. in mehreren Threads. Je nach Anwendungsfall kann Caching in im Repository oder in Datenquellenklassen.
Ergebnis der Netzwerkanfrage im Cache speichern
Der Einfachheit halber verwendet NewsRepository
eine änderbare Variable, um die neueste
Nachrichten. Zum Schutz von Lese- und Schreibvorgängen aus verschiedenen Threads wird ein
Mutex
verwendet wird. Weitere Informationen zum gemeinsamen änderbaren Status und zur Gleichzeitigkeit finden Sie in der
Kotlin
Dokumentation.
Mit der folgenden Implementierung werden die neuesten Nachrichteninformationen in einer Variablen in
Das Repository, das mit einem Mutex
schreibgeschützt ist. Wenn das Ergebnis der
Netzwerkanfrage erfolgreich ist, werden die Daten der Variablen latestNews
zugewiesen.
class NewsRepository(
private val newsRemoteDataSource: NewsRemoteDataSource
) {
// Mutex to make writes to cached values thread-safe.
private val latestNewsMutex = Mutex()
// Cache of the latest news got from the network.
private var latestNews: List<ArticleHeadline> = emptyList()
suspend fun getLatestNews(refresh: Boolean = false): List<ArticleHeadline> {
if (refresh || latestNews.isEmpty()) {
val networkResult = newsRemoteDataSource.fetchLatestNews()
// Thread-safe write to latestNews
latestNewsMutex.withLock {
this.latestNews = networkResult
}
}
return latestNewsMutex.withLock { this.latestNews }
}
}
Vorgang länger als den Bildschirm anzeigen
Wenn der Nutzer den Bildschirm verlässt, während die Netzwerkanfrage aktiv ist
ausgeführt haben, wird er abgebrochen und das Ergebnis wird nicht im Cache gespeichert. NewsRepository
sollte zum Ausführen dieser Logik nicht die CoroutineScope
des Aufrufers verwenden. Stattdessen
NewsRepository
sollte eine CoroutineScope
verwenden, die an ihren Lebenszyklus angehängt ist.
Das Abrufen aktueller Nachrichten muss apporientiert sein.
Um den Best Practices für die Abhängigkeitsinjektion zu folgen, sollte NewsRepository
eine
als Parameter in seinem Konstruktor verwenden, anstatt einen eigenen Bereich zu erstellen.
CoroutineScope
. Da Repositories die meiste Arbeit in
Hintergrundthreads nicht verwenden, sollten Sie den CoroutineScope
mit einer der folgenden Optionen konfigurieren:
Dispatchers.Default
oder mit Ihrem eigenen Thread-Pool.
class NewsRepository(
...,
// This could be CoroutineScope(SupervisorJob() + Dispatchers.Default).
private val externalScope: CoroutineScope
) { ... }
Da NewsRepository
bereit ist, anwendungsorientierte Vorgänge mit dem
externen CoroutineScope
enthält, muss der Aufruf an die Datenquelle erfolgen und
Ergebnis mit einer neuen Koroutine, die von diesem Bereich gestartet wurde:
class NewsRepository(
private val newsRemoteDataSource: NewsRemoteDataSource,
private val externalScope: CoroutineScope
) {
/* ... */
suspend fun getLatestNews(refresh: Boolean = false): List<ArticleHeadline> {
return if (refresh) {
externalScope.async {
newsRemoteDataSource.fetchLatestNews().also { networkResult ->
// Thread-safe write to latestNews.
latestNewsMutex.withLock {
latestNews = networkResult
}
}
}.await()
} else {
return latestNewsMutex.withLock { this.latestNews }
}
}
}
Mit async
wird die Koroutine im externen Bereich gestartet. await
wird aufgerufen
für die neue Koroutine anzuhalten, bis die Netzwerkanfrage zurückgegeben wird und der
wird das Ergebnis im Cache gespeichert. Wenn die Nutzenden zu diesem Zeitpunkt noch auf dem Bildschirm zu sehen sind,
werden die neuesten Nachrichten angezeigt. wenn sich die Nutzenden vom Bildschirm entfernen,
await
wird abgebrochen, aber die Logik in async
wird weiter ausgeführt.
Lesen Sie diesen Blog.
Beitrag
finden Sie weitere Informationen zu Mustern für CoroutineScope
.
Daten speichern und von der Festplatte abrufen
Angenommen, Sie möchten Daten wie als Lesezeichen gespeicherte Nachrichten und Nutzereinstellungen speichern. Diese Art von Daten muss überleben, selbst wenn Nutzer ist nicht mit dem Netzwerk verbunden.
Wenn die Daten, mit denen Sie arbeiten, den Prozesstod überleben müssen, Sie können ihn auf eine der folgenden Arten auf der Festplatte speichern:
- Für große Datasets, die abgefragt werden müssen, referenzielle Integrität benötigen oder Teilaktualisierungen benötigen, speichern Sie die Daten in einer Raumdatenbank. In der News App können beispielsweise die Nachrichtenartikel oder Autoren in der Datenbank gespeichert werden.
- Für kleine Datasets, die nur abgerufen und festgelegt werden müssen (keine Abfragen oder teilweise aktualisiert), verwenden Sie DataStore. Im Beispiel der News App bevorzugtes Datumsformat oder andere Anzeigeeinstellungen DataStore.
- Verwenden Sie für Datenblöcke wie ein JSON-Objekt eine Datei.
Wie im Abschnitt Source of Truth erwähnt, sind alle Daten
Quelle funktioniert nur mit einer Quelle und entspricht einem bestimmten Datentyp (für
Beispiel: News
, Authors
, NewsAndAuthors
oder UserPreferences
). Klassen
in denen die Datenquelle verwendet wird,
dürfen nicht wissen, wie die Daten gespeichert werden,
Datenbank oder in einer Datei.
Raum als Datenquelle
Jede Datenquelle sollte nur mit jeweils einer Datenquelle arbeiten.
Datenquelle für einen bestimmten Datentyp erstellt, erhält eine Zimmerdatenquelle entweder ein
Datenzugriffsobjekt (Data Access Object, DAO) oder den
Datenbank selbst als Parameter. NewsLocalDataSource
kann z. B. einen
Instanz von NewsDao
als Parameter. Für AuthorsLocalDataSource
kann eine
Instanz von AuthorsDao
.
In einigen Fällen können Sie den DAO direkt einfügen, wenn keine zusätzliche Logik erforderlich ist. in das Repository importieren, da DAO eine Schnittstelle ist, die Sie ganz einfach Tests durchführen.
Weitere Informationen zur Verwendung der Room APIs finden Sie im Leitfäden.
DataStore als Datenquelle
DataStore eignet sich perfekt zum Speichern Schlüssel/Wert-Paare wie Nutzereinstellungen. Beispiele hierfür sind das Zeitformat, und ob Nachrichtenelemente nach der Nutzeraktion ein- oder ausgeblendet werden sollen. sie gelesen hat. DataStore kann auch typisierte Objekte mit Protokoll Puffer.
Wie bei jedem anderen Objekt sollte eine von DataStore unterstützte Datenquelle Folgendes enthalten: Daten, die einem bestimmten Typ oder einem bestimmten Teil der App entsprechen. Dies ist Dies gilt noch mehr für DataStore, da DataStore-Lesevorgänge als ein Fluss die bei jeder Aktualisierung eines Werts ausgegeben wird. Aus diesem Grund sollten Sie verwandten Einstellungen im selben DataStore.
Beispielsweise könnten Sie ein NotificationsDataStore
haben, das
und eine NewsPreferencesDataStore
, die nur
verarbeitet Einstellungen in Bezug auf den Nachrichtenbildschirm. So können Sie den Umfang
die Aktualisierungen besser machen, da nur der newsScreenPreferencesDataStore.data
-Ablauf
wird ausgelöst, wenn eine Einstellung für diesen Bildschirm geändert wird. Es bedeutet auch, dass
kann der Lebenszyklus des Objekts kürzer sein, da es nur so lange leben kann,
wird der Nachrichtenbildschirm angezeigt.
Weitere Informationen zur Arbeit mit den DataStore APIs finden Sie im DataStore Leitfäden.
Eine Datei als Datenquelle
Wenn Sie mit großen Objekten wie einem JSON-Objekt oder einer Bitmap arbeiten,
mit einem File
-Objekt arbeiten und Wechsel-Threads handhaben.
Weitere Informationen zur Arbeit mit Dateispeichern findest du im Speicher .
Aufgaben mit WorkManager planen
Angenommen, für die Nachrichten-App wird eine weitere neue Anforderung eingeführt: Die App muss dem Nutzer die Möglichkeit geben, die neuesten Nachrichten regelmäßig und automatisch abzurufen, solange das Gerät geladen wird und mit einem nicht getakteten Netzwerk verbunden ist. Das macht ein geschäftsorientiertes Unternehmen. Durch diese Anforderung ist es sogar möglich, Wenn das Gerät beim Öffnen der App keine Internetverbindung hat, kann weiterhin aktuelle Nachrichten sehen.
WorkManager macht es einfach,
asynchrone und zuverlässige Arbeit einplanen und
zu verstehen. Dies ist die empfohlene Bibliothek für dauerhafte Arbeit. Um die
die oben definierte Aufgabe, ein
Worker
Klasse wurde erstellt: RefreshLatestNewsWorker
. Dieser Kurs dauert NewsRepository
als Abhängigkeit, um die neuesten Nachrichten abzurufen und sie im Cache auf dem Laufwerk zu speichern.
class RefreshLatestNewsWorker(
private val newsRepository: NewsRepository,
context: Context,
params: WorkerParameters
) : CoroutineWorker(context, params) {
override suspend fun doWork(): Result = try {
newsRepository.refreshLatestNews()
Result.success()
} catch (error: Throwable) {
Result.failure()
}
}
Die Geschäftslogik für diesen Aufgabentyp sollte in einer eigenen Klasse gekapselt werden. und als separate Datenquelle behandelt. WorkManager ist dann nur noch um sicherzustellen, dass die Arbeit in einem Hintergrundthread ausgeführt wird, wenn alle Einschränkungen erfüllt sind. Wenn Sie sich an dieses Muster halten, können Sie Implementierungen Umgebungen nach Bedarf anpassen.
In diesem Beispiel muss die nachrichtenbezogene Aufgabe aus NewsRepository
aufgerufen werden,
Dies würde eine neue Datenquelle als Abhängigkeit annehmen: NewsTasksDataSource
,
wie folgt implementiert:
private const val REFRESH_RATE_HOURS = 4L
private const val FETCH_LATEST_NEWS_TASK = "FetchLatestNewsTask"
private const val TAG_FETCH_LATEST_NEWS = "FetchLatestNewsTaskTag"
class NewsTasksDataSource(
private val workManager: WorkManager
) {
fun fetchNewsPeriodically() {
val fetchNewsRequest = PeriodicWorkRequestBuilder<RefreshLatestNewsWorker>(
REFRESH_RATE_HOURS, TimeUnit.HOURS
).setConstraints(
Constraints.Builder()
.setRequiredNetworkType(NetworkType.TEMPORARILY_UNMETERED)
.setRequiresCharging(true)
.build()
)
.addTag(TAG_FETCH_LATEST_NEWS)
workManager.enqueueUniquePeriodicWork(
FETCH_LATEST_NEWS_TASK,
ExistingPeriodicWorkPolicy.KEEP,
fetchNewsRequest.build()
)
}
fun cancelFetchingNewsPeriodically() {
workManager.cancelAllWorkByTag(TAG_FETCH_LATEST_NEWS)
}
}
Diese Klassentypen sind nach den Daten benannt, für die sie verantwortlich sind –
Beispiel: NewsTasksDataSource
oder PaymentsTasksDataSource
. Alle zugehörigen Aufgaben
Daten eines bestimmten Typs
in derselben Klasse gekapselt sein.
Wenn die Aufgabe beim Start der App ausgelöst werden muss, wird empfohlen, sie auszulösen
WorkManager-Anfrage über den App-Start
die das Repository von einem
Initializer
Weitere Informationen zur Arbeit mit WorkManager APIs finden Sie in der WorkManager API Leitfäden.
Testen
Best Practices für die Abhängigkeitsinjektion helfen Ihnen, Ihre App testen. Es ist auch hilfreich, sich auf Schnittstellen für Klassen zu verlassen, mit externen Ressourcen kommunizieren können. Wenn Sie eine Einheit testen, können Sie eine fiktive Versionen seiner Abhängigkeiten, um den Test deterministisch und zuverlässig zu machen.
Einheitentests
Beim Testen der Daten gelten allgemeine Testrichtlinien. Ebene. Verwenden Sie für Unittests bei Bedarf echte Objekte und fälschen Sie alle Abhängigkeiten die externe Quellen wie das Lesen aus einer Datei oder Netzwerk.
Integrationstests
Integrationstests, die auf externe Quellen zugreifen, sind tendenziell weniger deterministisch weil sie auf einem echten Gerät ausgeführt werden müssen. Es empfiehlt sich, diese Tests in einer kontrollierten Umgebung, um die Integrationstests zuverlässig sind.
Für Datenbanken können Sie in Room eine speicherinterne Datenbank erstellen, Kontrolle in Ihren Tests. Weitere Informationen erhalten Sie im Artikel Testen und Debuggen von Datenbank.
Für Networking gibt es beliebte Bibliotheken wie WireMock oder MockWebServer mit denen Sie HTTP- und HTTPS-Aufrufe fälschen und überprüfen können, ob die Anfragen wie folgt gesendet wurden: zu erwarten war.
Produktproben
In den folgenden Google-Beispielen wird die Verwendung der Datenschicht veranschaulicht. Sehen Sie sich diese Tipps in der Praxis an:
Empfehlungen für dich
- Hinweis: Der Linktext wird angezeigt, wenn JavaScript deaktiviert ist.
- Domainebene
- Eine Offline-App erstellen
- UI State-Produktion