Leitfaden zur App-Architektur

Die App-Architektur ist die Grundlage einer hochwertigen Android-App. Mit einer gut definierten Architektur können Sie eine skalierbare und wartungsfreundliche App erstellen, die sich an das ständig wachsende Ökosystem von Android-Geräten anpassen lässt, darunter Smartphones, Tablets, Foldables, ChromeOS-Geräte, Autodisplays und XR-Geräte.

App-Zusammensetzung

Eine typische Android-App besteht aus mehreren App-Komponenten, z. B. Diensten, Content-Providern und Broadcast-Empfängern. Sie deklarieren diese Komponenten in Ihrem App-Manifest.

Die Benutzeroberfläche einer App ist ebenfalls eine Komponente. Bisher wurden UIs mit mehreren Aktivitäten erstellt. Moderne Apps verwenden jedoch eine Architektur mit nur einer Aktivität. Ein einzelnes Activity dient als Container für Bildschirme oder Jetpack Compose-Ziele.

Mehrere Formfaktoren

Apps können auf verschiedenen Formfaktoren ausgeführt werden, darunter nicht nur Smartphones, sondern auch Tablets, Foldables und ChromeOS-Geräte. Gehen Sie nicht davon aus, dass Ihre App immer im Hoch- oder Querformat angezeigt wird. Konfigurationsänderungen wie das Drehen des Geräts oder das Auf- und Zuklappen eines faltbaren Geräts zwingen Ihre App, die Benutzeroberfläche neu zu erstellen, was sich auf den App-Status auswirkt.

Ressourcenlimits

Mobilgeräte, auch solche mit großen Displays, haben nur begrenzte Ressourcen. Das Betriebssystem kann den App-Prozess jederzeit beenden, um seine Ressourcen anderen Prozessen zur Verfügung zu stellen.

Variablen-Startbedingungen

In einer Umgebung mit begrenzten Ressourcen können die Komponenten Ihrer App einzeln und in beliebiger Reihenfolge gestartet werden. Außerdem können sie jederzeit vom Betriebssystem oder vom Nutzer beendet werden. Speichern Sie daher keine Anwendungsdaten oder keinen Anwendungsstatus in Ihren App-Komponenten. App-Komponenten sollten in sich geschlossen und unabhängig voneinander sein.

Allgemeine Architekturprinzipien

Wenn Sie keine App-Komponenten zum Speichern von Anwendungsdaten und ‑status verwenden können, wie sollten Sie Ihre App dann gestalten?

Da Android-Apps immer größer werden, ist es wichtig, eine Architektur zu definieren, die es ermöglicht, die App zu skalieren. Eine gut durchdachte App-Architektur definiert die Grenzen zwischen den einzelnen Teilen der App und die Verantwortlichkeiten der einzelnen Teile.

Trennung der Zuständigkeiten

Entwerfen Sie Ihre App-Architektur nach einigen bestimmten Prinzipien.

Das wichtigste Prinzip ist die Trennung von Belangen: Ihre App wird in Methoden, Klassen, Dateien, Pakete, Module und Ebenen unterteilt, die klar definierte Verantwortlichkeiten und Grenzen haben.

Es ist ein häufiger Fehler, den gesamten Code in ein Activity zu schreiben.

Die primäre Rolle eines Activity ist das Hosten der Benutzeroberfläche Ihrer App. Der Lebenszyklus von Fragmenten wird vom Android-Betriebssystem gesteuert. Sie werden häufig als Reaktion auf Nutzeraktionen wie die Bildschirmdrehung oder Systemereignisse wie wenig Arbeitsspeicher zerstört und neu erstellt.

Aufgrund dieser Flüchtigkeit eignen sie sich nicht zum Speichern von Anwendungsdaten oder ‑status. Wenn Sie Daten in einem Activity speichern, gehen diese Daten verloren, wenn die Komponente neu erstellt wird. Um die Datenpersistenz zu gewährleisten und eine stabile Nutzererfahrung zu bieten, sollten Sie diesen UI-Komponenten keinen Status anvertrauen.

Adaptive Layouts

Entwickeln Sie Apps, die Konfigurationsänderungen wie Änderungen der Geräteausrichtung oder Änderungen der Größe des App-Fensters problemlos verarbeiten können. Implementieren Sie die adaptiven kanonischen Layouts, um auf verschiedenen Formfaktoren eine optimale Nutzererfahrung zu bieten.

Benutzeroberfläche über Datenmodelle steuern

Ein weiterer wichtiger Grundsatz ist, die Benutzeroberfläche aus Datenmodellen abzuleiten, vorzugsweise aus persistenten Modellen. Datenmodelle stellen die Daten einer App dar. Sie sind unabhängig von den UI-Elementen und anderen Komponenten in Ihrer App. Das bedeutet, dass sie nicht an den Lebenszyklus der UI und der App-Komponenten gebunden sind, aber trotzdem zerstört werden, wenn das Betriebssystem den Prozess der App aus dem Arbeitsspeicher entfernt.

Persistente Modelle sind aus folgenden Gründen ideal:

  • Nutzer verlieren keine Daten, wenn das Android-Betriebssystem Ihre App beendet, um Ressourcen freizugeben.

  • Ihre App funktioniert auch dann, wenn die Netzwerkverbindung zeitweise unterbrochen oder nicht verfügbar ist.

Richten Sie die Architektur Ihrer App an Datenmodellklassen aus, um sie robust und testbar zu machen.

Zentrale Datenquelle

Wenn in Ihrer App ein neuer Datentyp definiert wird, weisen Sie ihm eine einzige Quelle der Wahrheit (Single Source of Truth, SSOT) zu. Das SSOT ist der Inhaber dieser Daten und nur das SSOT kann sie ändern oder mutieren. Dazu stellt die SSOT die Daten mit einem unveränderlichen Typ bereit. Um die Daten zu ändern, stellt die SSOT Funktionen bereit oder empfängt Ereignisse, die von anderen Typen aufgerufen werden können.

Dieses Muster bietet mehrere Vorteile:

  • Alle Änderungen an einem bestimmten Datentyp werden an einem Ort zentralisiert.
  • Schützt die Daten vor Manipulationen durch andere Typen
  • Änderungen an den Daten sind besser nachvollziehbar, sodass Fehler leichter zu erkennen sind.

In einer Offline-First-Anwendung ist die Quelle der Wahrheit für Anwendungsdaten in der Regel eine Datenbank. In anderen Fällen kann die Source of Truth ein ViewModel sein.

Unidirektionaler Datenfluss

Das Prinzip der Single Source of Truth wird häufig mit dem Muster des unidirektionalen Datenflusses (Unidirectional Data Flow, UDF) verwendet. In benutzerdefinierten Funktionen fließt der Status nur in eine Richtung, in der Regel von der übergeordneten zur untergeordneten Komponente. Die Ereignisse, die den Datenfluss in die entgegengesetzte Richtung ändern.

In Android fließen Status oder Daten normalerweise von den Typen mit höherem Bereich in der Hierarchie zu den Typen mit niedrigerem Bereich. Ereignisse werden in der Regel von den Typen mit niedrigerem Umfang ausgelöst, bis sie den SSOT für den entsprechenden Datentyp erreichen. Anwendungsdaten fließen beispielsweise in der Regel von Datenquellen zur Benutzeroberfläche. Nutzerereignisse wie Tastendrücke werden von der Benutzeroberfläche an die SSOT weitergeleitet, wo die Anwendungsdaten geändert und in einem unveränderlichen Typ bereitgestellt werden.

Dieses Muster sorgt für eine bessere Datenkonsistenz, ist weniger fehleranfällig, lässt sich leichter debuggen und bietet alle Vorteile des SSOT-Musters.

Weitere Informationen zu UDF finden Sie unter Unidirektionaler Datenfluss in Jetpack Compose.

Berücksichtigen Sie gängige Architekturprinzipien und entwerfen Sie jede Anwendung mit mindestens zwei Ebenen:

  • UI-Ebene:Anwendungsdaten werden auf dem Bildschirm angezeigt.
  • Datenebene:Enthält die Geschäftslogik Ihrer App und stellt Anwendungsdaten bereit.

Sie können eine zusätzliche Ebene namens Domänenebene hinzufügen, um die Interaktionen zwischen der UI- und der Datenschicht zu vereinfachen und wiederzuverwenden.

In einer typischen App-Architektur ruft die UI-Schicht die Anwendungsdaten aus der Datenschicht oder aus der optionalen Domänenschicht ab, die sich zwischen der UI-Schicht und der Datenschicht befindet.
Abbildung 1: Diagramm einer typischen App-Architektur.

Moderne App-Architektur

Eine moderne Android-App-Architektur verwendet unter anderem die folgenden Techniken:

  • Adaptive und mehrschichtige Architektur
  • Unidirektionaler Datenfluss (UDF) in allen Ebenen der App
  • UI-Ebene mit Statusinhabern zur Verwaltung der Komplexität der Benutzeroberfläche
  • Coroutinen und Flows
  • Best Practices für die Abhängigkeitsinjektion
  • Leistungsoptimierung mit R8 und Baseline-Profilen und Messung mit Macrobenchmark

Weitere Informationen finden Sie unter Empfehlungen für die Android-Architektur.

UI-Ebene

Die Aufgabe der UI-Ebene (oder Darstellungsebene) besteht darin, die Anwendungsdaten auf dem Bildschirm anzuzeigen. Immer wenn sich die Daten ändern, entweder durch Nutzerinteraktion (z. B. durch Drücken einer Schaltfläche) oder durch externe Eingaben (z. B. eine Netzwerkantwort), wird die Benutzeroberfläche aktualisiert, um die Änderungen widerzuspiegeln.

Die UI-Ebene umfasst zwei Arten von Konstrukten:

  • UI-Elemente, die die Daten auf dem Bildschirm rendern. Sie erstellen diese Elemente mit Jetpack Compose-Funktionen, um adaptive Layouts zu unterstützen.
  • Statusinhaber (z. B. ViewModel), die Daten enthalten, sie für die Benutzeroberfläche bereitstellen und die Logik verarbeiten. Statusinhaber sollten so lange bestehen wie das UI-Element, für das sie den Status bereitstellen. Ein ViewModel für einen Bildschirm sollte beispielsweise im Arbeitsspeicher bleiben, bis der Bildschirm aus dem Navigations-Backstack der App entfernt wird. Weitere Informationen finden Sie unter Lebensdauer von Status.
In einer typischen Architektur hängen die UI-Elemente der UI-Schicht von Status-Holdern ab, die wiederum von Klassen aus der Datenschicht oder der optionalen Domänenschicht abhängen.
Abbildung 2. Die Rolle der UI-Ebene in der App-Architektur.

Bei adaptiven UIs stellen State-Holder wie ViewModel-Objekte UI-Zustände bereit, die sich an verschiedene Fenstergrößenklassen anpassen. Sie können currentWindowAdaptiveInfo() verwenden, um diesen UI-Status abzuleiten. Komponenten wie NavigationSuiteScaffold können diese Informationen dann verwenden, um je nach verfügbarem Bildschirmplatz automatisch zwischen verschiedenen Navigationsmustern (z. B. NavigationBar, NavigationRail oder NavigationDrawer) zu wechseln.

Weitere Informationen finden Sie unter UI-Layer und Compose-UI-Architektur.

Weitere Informationen zu adaptiven Apps und zur adaptiven Navigation finden Sie unter Adaptive Apps entwickeln und Adaptive Navigation entwickeln.

Datenebene

Die Datenschicht einer App enthält die Geschäftslogik. Die Geschäftslogik ist das, was Ihrer App Wert verleiht. Sie umfasst Regeln, die bestimmen, wie Ihre App Daten erstellt, speichert und ändert.

Die Datenschicht besteht aus Repositories, die jeweils null bis viele Datenquellen enthalten können. Erstellen Sie für jeden Datentyp, den Sie in Ihrer App verarbeiten, eine Repository-Klasse. Sie können beispielsweise eine MoviesRepository-Klasse für Daten im Zusammenhang mit Filmen oder eine PaymentsRepository-Klasse für Daten im Zusammenhang mit Zahlungen erstellen.

In einer typischen Architektur stellen die Repositories der Datenschicht Daten für den Rest der App bereit und sind von den Datenquellen abhängig.
Abbildung 3. Die Rolle der Datenschicht in der App-Architektur.

Repository-Klassen sind für Folgendes verantwortlich:

  • Daten für den Rest der App verfügbar machen
  • Änderungen an den Daten zentralisieren
  • Konflikte zwischen mehreren Datenquellen beheben
  • Datenquellen vom Rest der App abstrahieren
  • Geschäftslogik

Jede Datenquellenklasse ist nur für die Verarbeitung einer Datenquelle verantwortlich. Das kann eine Datei, eine Netzwerkquelle oder eine lokale Datenbank sein. Datenquellenklassen sind die Brücke zwischen der Anwendung und dem System für Datenvorgänge.

Weitere Informationen

Domänenebene

Die Domänenebene ist eine optionale Ebene zwischen der UI- und der Datenschicht.

Die Domänenschicht ist dafür verantwortlich, komplexe Geschäftslogik oder einfachere Geschäftslogik zu kapseln, die von mehreren Viewmodels wiederverwendet wird. Die Domänenebene ist optional, da nicht alle Apps diese Anforderungen erfüllen. Verwenden Sie sie nur, wenn sie erforderlich sind, z. B. um Komplexität zu bewältigen oder die Wiederverwendbarkeit zu fördern.

Wenn sie enthalten ist, stellt die optionale Domain-Schicht Abhängigkeiten für die UI-Schicht bereit und hängt von der Datenschicht ab.
Abbildung 4: Die Rolle der Domänenschicht in der App-Architektur.

Klassen in der Domänenschicht werden häufig als Anwendungsfälle oder Interaktoren bezeichnet. Jeder Anwendungsfall ist für eine einzelne Funktion verantwortlich. Ihre App könnte beispielsweise die Klasse GetTimeZoneUseCase haben, wenn mehrere Viewmodels Zeitzonen verwenden, um die richtige Nachricht auf dem Bildschirm anzuzeigen.

Weitere Informationen finden Sie auf der Seite zur Domänenebene.

Abhängigkeiten zwischen Komponenten verwalten

Klassen in Ihrer App sind von anderen Klassen abhängig, damit sie richtig funktionieren. Sie können eines der folgenden Designmuster verwenden, um die Abhängigkeiten einer bestimmten Klasse zu erfassen:

  • Dependency Injection (DI): Mit Dependency Injection können Klassen ihre Abhängigkeiten definieren, ohne sie zu erstellen. Zur Laufzeit ist eine andere Klasse für die Bereitstellung dieser Abhängigkeiten verantwortlich.
  • Service Locator: Das Service Locator-Muster bietet eine Registry, in der Klassen ihre Abhängigkeiten abrufen können, anstatt sie zu erstellen.

Mit diesen Mustern können Sie Ihren Code skalieren, da sie klare Muster für die Verwaltung von Abhängigkeiten bieten, ohne Code zu duplizieren oder Komplexität hinzuzufügen. Mit den Mustern können Sie auch schnell zwischen Test- und Produktionsimplementierungen wechseln.

Allgemeine Best Practices

Programmieren ist ein kreatives Feld und das Erstellen von Android-Apps ist da keine Ausnahme. Es gibt viele Möglichkeiten, ein Problem zu lösen. Sie können Daten zwischen mehreren Aktivitäten oder Fragmenten übertragen, Remote-Daten abrufen und für den Offlinemodus lokal speichern oder eine beliebige Anzahl anderer häufiger Szenarien verarbeiten, die in nicht trivialen Apps auftreten.

Die folgenden Empfehlungen sind zwar nicht zwingend erforderlich, aber in den meisten Fällen wird Ihre Codebasis dadurch robuster, testbarer und wartungsfreundlicher.

Daten nicht in App-Komponenten speichern:

Vermeiden Sie es, die Einstiegspunkte Ihrer App wie Aktivitäten, Dienste und Broadcast-Empfänger als Datenquellen festzulegen. Die Einstiegspunkte müssen mit anderen Komponenten koordiniert werden, damit nur die für den jeweiligen Einstiegspunkt relevanten Daten abgerufen werden. Jede App-Komponente ist kurzlebig und hängt von der Interaktion des Nutzers mit seinem Gerät und der Kapazität des Systems ab.

Abhängigkeiten von Android-Klassen reduzieren:

Sorgen Sie dafür, dass nur Ihre App-Komponenten auf Android-Framework-SDK-APIs wie Context oder Toast angewiesen sind. Wenn Sie andere Klassen in Ihrer App von den App-Komponenten abstrahieren, wird die Testbarkeit verbessert und die Kopplung in Ihrer App reduziert.

Definieren Sie klare Verantwortlichkeitsgrenzen zwischen den Modulen in Ihrer App.

Verteilen Sie den Code, der Daten aus dem Netzwerk lädt, nicht auf mehrere Klassen oder Pakete in Ihrer Codebasis. Definieren Sie auch nicht mehrere unabhängige Verantwortlichkeiten wie Daten-Caching und Datenbindung in derselben Klasse. Halten Sie sich an die empfohlene App-Architektur.

So wenig wie möglich von jedem Modul offenlegen:

Erstellen Sie keine Verknüpfungen, die interne Implementierungsdetails offenlegen. Kurzfristig sparen Sie vielleicht etwas Zeit, aber mit der Weiterentwicklung Ihrer Codebasis werden Sie wahrscheinlich um ein Vielfaches mehr technische Schulden anhäufen.

Konzentrieren Sie sich auf das Alleinstellungsmerkmal Ihrer App, damit sie sich von anderen Apps abhebt.

Sie müssen nicht immer wieder denselben Boilerplate-Code schreiben. Konzentrieren Sie sich stattdessen auf das, was Ihre App einzigartig macht. Lassen Sie die Jetpack-Bibliotheken und andere empfohlene Bibliotheken die sich wiederholenden Boilerplate-Codes übernehmen.

Kanonische Layouts und App-Designmuster verwenden:

Die Jetpack Compose-Bibliotheken bieten leistungsstarke APIs zum Erstellen adaptiver Benutzeroberflächen. Verwenden Sie die kanonischen Layouts in Ihrer App, um die Nutzerfreundlichkeit auf mehreren Formfaktoren und Displaygrößen zu optimieren. Sehen Sie sich die Galerie mit App-Designmustern an, um die Layouts auszuwählen, die am besten für Ihre Anwendungsfälle geeignet sind.

UI-Status bei Konfigurationsänderungen beibehalten:

Wenn Sie adaptive Layouts entwerfen, sollten Sie den UI-Status bei Konfigurationsänderungen wie dem Anpassen der Displaygröße, dem Auf- und Zuklappen und dem Ändern der Ausrichtung beibehalten. Ihre Architektur sollte sicherstellen, dass der aktuelle Status des Nutzers beibehalten wird, um eine reibungslose Nutzung zu ermöglichen.

Wiederverwendbare und zusammensetzbare UI-Komponenten entwerfen:

Erstellen Sie UI-Komponenten, die wiederverwendbar und zusammensetzbar sind, um adaptives Design zu unterstützen. So können Sie Komponenten kombinieren und neu anordnen, um sie ohne großen Aufwand an verschiedene Bildschirmgrößen und Ausrichtungen anzupassen.

Überlegen Sie, wie Sie jeden Teil Ihrer App isoliert testbar machen können.

Eine gut definierte API zum Abrufen von Daten aus dem Netzwerk erleichtert das Testen des Moduls, das diese Daten in einer lokalen Datenbank speichert. Wenn Sie die Logik dieser beiden Funktionen stattdessen an einem Ort zusammenfassen oder Ihren Netzwerkcode über die gesamte Codebasis verteilen, wird das Testen viel schwieriger, wenn nicht sogar unmöglich.

Typen sind für ihre Nebenläufigkeitsrichtlinie verantwortlich.

Wenn ein Typ blockierende Vorgänge mit langer Ausführungszeit ausführt, sollte er dafür sorgen, dass die Berechnung im richtigen Thread erfolgt. Der Typ weiß, welche Art von Berechnung er durchführt und in welchem Thread die Berechnung ausgeführt werden soll. Typen sollten „main-safe“ sein, d. h., sie können sicher aus dem Hauptthread aufgerufen werden, ohne ihn zu blockieren.

So viele relevante und aktuelle Daten wie möglich beibehalten:

So können Nutzer die Funktionen Ihrer App auch dann nutzen, wenn sich ihr Gerät im Offlinemodus befindet. Denken Sie daran, dass nicht alle Nutzer eine konstante Highspeed-Verbindung haben. Selbst wenn das der Fall ist, kann es an überfüllten Orten zu Empfangsproblemen kommen.

Vorteile der Architektur

Eine gut implementierte Architektur in Ihrer App bietet viele Vorteile für das Projekt und die Entwicklerteams:

  • Verbessert die Wartungsfreundlichkeit, Qualität und Stabilität der gesamten App.
  • Ermöglicht die Skalierung der App. Mehr Personen und mehr Teams können mit minimalen Codekonflikten zur selben Codebasis beitragen.
  • Hilft beim Onboarding. Da die Architektur für Konsistenz in Ihrem Projekt sorgt, können sich neue Teammitglieder schnell einarbeiten und in kürzerer Zeit effizienter arbeiten.
  • Sie sind einfacher zu testen. Eine gute Architektur fördert einfachere Typen, die in der Regel leichter zu testen sind.
  • Sie können Fehler mithilfe von klar definierten Prozessen systematisch untersuchen.

Eine gute Architektur erfordert zwar eine Vorabinvestition von Zeit, hat aber auch einen direkten Einfluss auf die Nutzer. Sie profitieren von einer stabileren Anwendung und mehr Funktionen, da das Entwicklerteam produktiver ist.

Beispiele

Die folgenden Beispiele zeigen eine gute App-Architektur: