Leitfaden zur App-Architektur

Die App-Architektur ist die Grundlage für eine hochwertige Android-Anwendung. 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, faltbare Geräte, ChromeOS-Geräte, Fahrzeugdisplays 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, faltbare Geräte 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.

Ressourcenbeschränkungen

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

Bedingungen für die Einführung von Variablen

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 App-Komponenten nicht 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.

Aufteilung der Zuständigkeiten

Die Architektur Ihrer App sollte einigen bestimmten Prinzipien folgen.

Das wichtigste Prinzip ist die Trennung von Belangen: Die 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 Aufgabe eines Activity ist es, die Benutzeroberfläche Ihrer App zu hosten. Der Lebenszyklus von Aktivitäten 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 Kurzlebigkeit 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 Nutzerfreundlichkeit zu bieten.

Benutzeroberfläche aus Datenmodellen ableiten

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 App-Komponenten gebunden sind, aber trotzdem zerstört werden, wenn das Betriebssystem den Prozess der App aus dem Arbeitsspeicher entfernt.

Dauerhafte 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 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 werden die Daten im SSOT über einen unveränderlichen Typ bereitgestellt. Zum Ändern der Daten stellt der 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 Quelle der Wahrheit 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 UDF fließt der state 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 Unidirectional data flow in Jetpack Compose.

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

  • UI-Ebene:Anwendungsdaten auf dem Bildschirm anzeigen
  • 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

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

UI-Ebene

Die Aufgabe der UI-Schicht (oder Darstellungsschicht) 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 in der Benutzeroberfläche verfügbar machen 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 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 State-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.

Datenschicht

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 Repositorys 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 bereitstellen
  • Änderungen an den Daten zentralisieren
  • Konflikte zwischen mehreren Datenquellen beheben
  • Datenquellen vom Rest der App abstrahieren
  • Geschäftslogik

Jede Datenquellenklasse ist dafür verantwortlich, nur mit einer Datenquelle zu arbeiten. 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 Datenebene.

Die Domänenschicht ist für die Kapselung komplexer Geschäftslogik oder einfacherer Geschäftslogik verantwortlich, die von mehreren Ansichtsmodellen wiederverwendet wird. Die Domänenebene ist optional, da nicht alle Apps diese Anforderungen erfüllen. Verwenden Sie sie nur bei Bedarf, z. B. um Komplexität zu bewältigen oder die Wiederverwendbarkeit zu fördern.

Wenn sie enthalten ist, bietet die optionale Domain-Schicht Abhängigkeiten von der UI-Schicht und hängt von der Datenschicht ab.
Abbildung 4: Die Rolle der Domänenebene in der App-Architektur.

Klassen in der Domänenebene werden häufig als Anwendungsfälle oder Interaktoren bezeichnet. Jeder Anwendungsfall ist für eine einzelne Funktion verantwortlich. Ihre App könnte beispielsweise eine GetTimeZoneUseCase-Klasse haben, wenn mehrere Viewmodels auf Zeitzonen angewiesen sind, 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 für die ordnungsgemäße Funktion von anderen Klassen abhängig. 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 dass Code dupliziert oder die Komplexität erhöht wird. 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 Ihr Code dadurch robuster, testbarer und wartungsfreundlicher.

Speichern Sie keine Daten in App-Komponenten.

Vermeiden Sie es, die Einstiegspunkte Ihrer App wie Aktivitäten, Dienste und Broadcast-Empfänger als Datenquellen festzulegen. Sorgen Sie dafür, dass die Einstiegspunkte mit anderen Komponenten koordiniert werden, um nur die Teilmenge der Daten abzurufen, die für diesen Einstiegspunkt relevant ist. 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 Verantwortlichkeiten zwischen den Modulen in Ihrer App.

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

So wenig wie möglich aus jedem Modul verfügbar machen

Erstellen Sie keine Verknüpfungen, die interne Implementierungsdetails offenlegen. Kurzfristig sparen Sie vielleicht etwas Zeit, aber im Laufe der Zeit werden Sie wahrscheinlich umso mehr technischen 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 verschiedenen Formfaktoren und Displaygrößen zu optimieren. Sehen Sie sich die Galerie mit App-Designmustern an, um die Layouts auszuwählen, die für Ihre Anwendungsfälle am besten geeignet sind.

UI-Status bei Konfigurationsänderungen beibehalten

Bei der Entwicklung für adaptive Layouts muss der UI-Status bei Konfigurationsänderungen wie dem Ändern der Displaygröße, dem Auf- und Zuklappen und dem Ändern der Ausrichtung beibehalten werden. Ihre Architektur sollte sicherstellen, dass der aktuelle Status des Nutzers beibehalten wird, um eine nahtlose 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 größere Umstrukturierung an verschiedene Bildschirmgrößen und Ausrichtungen anzupassen.

Überlegen Sie, wie Sie jeden Teil Ihrer App isoliert testen 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 Richtlinie zur Parallelität verantwortlich.

Wenn ein Typ lange blockierende Aufgaben ausführt, sollte er dafür sorgen, dass die Berechnung im richtigen Thread erfolgt. Der Typ kennt die Art der Berechnung und den Thread, in dem die Berechnung ausgeführt werden soll. Typen sollten „main-safe“ sein. Das bedeutet, dass sie sicher vom Hauptthread aus aufgerufen werden können, 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 der Empfang an überfüllten Orten schlecht sein.

Vorteile der Architektur

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

  • Verbessert die Wartbarkeit, Qualität und Robustheit der gesamten App.
  • Ermöglicht das Skalieren der App. Mehr Personen und 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 lassen sich leichter 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.

Produktproben

Die folgenden Beispiele veranschaulichen eine gute App-Architektur: