Data-Ebene

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.
Abbildung 1: Die Rolle der Datenschicht in der Anwendungsarchitektur

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 oder Completable.
  • 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- oder Flowable-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.
Abbildung 2: Abhängigkeitsdiagramm eines Repositorys, das von anderen Repositories.

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: