Leitfaden zur App-Architektur

Die App-Architektur ist die Grundlage für eine hochwertige Android-Anwendung. Eine gut definierte Architektur ermöglicht es Ihnen, eine skalierbare und wartungsfreundliche App zu erstellen, die sich an das ständig wachsende Ökosystem von Android-Geräten anpassen kann, darunter Smartphones, Tablets, faltbare Geräte, 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, die als Fragmente oder Jetpack Compose-Ziele implementiert sind.

Mehrere Formfaktoren

Apps können auf verschiedenen Formfaktoren ausgeführt werden, darunter Smartphones, Tablets, faltbare Geräte und ChromeOS-Geräte. Eine App darf nicht von einer Hoch- oder Querformat-Ausrichtung ausgehen. 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. Dies wirkt sich auf App-Daten und den Status aus.

Ressourcenbeschränkungen

Mobilgeräte, auch solche mit großen Displays, haben nur begrenzte Ressourcen. Das Betriebssystem kann daher jederzeit einige App-Prozesse beenden, um Platz für neue zu schaffen.

Bedingungen für die Variableneinführung

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. Die 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 konzipierte App-Architektur definiert die Grenzen zwischen den einzelnen Teilen der App und die Verantwortlichkeiten, die jeder Teil haben sollte.

Aufteilung der Zuständigkeiten

Die Architektur Ihrer App sollte einigen bestimmten Prinzipien folgen.

Der wichtigste Grundsatz ist die Trennung von Belangen. Ein häufiger Fehler ist es, den gesamten Code in einem Activity oder einem Fragment zu schreiben.

Die primäre Rolle eines Activity oder Fragment besteht darin, die Benutzeroberfläche Ihrer App zu hosten. Der Lebenszyklus wird vom Android-Betriebssystem gesteuert. Die Fragmente 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 oder Fragment 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

Ihre App sollte 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, dass Sie die Benutzeroberfläche über Datenmodelle steuern sollten, vorzugsweise über persistente Modelle. 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.

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 unterbrochen oder nicht verfügbar ist.

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

Eine Single Source of Truth

Wenn in Ihrer App ein neuer Datentyp definiert wird, sollten Sie ihm eine einzige Quelle der Wahrheit (Single Source of Truth, SSOT) zuweisen. Das SSOT ist der Inhaber dieser Daten und nur das SSOT kann sie ändern. Dazu werden die Daten im SSOT über einen unveränderlichen Typ verfügbar gemacht. Zum Ändern der Daten werden Funktionen verfügbar gemacht oder Ereignisse empfangen, 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 UDF fließt der state nur in eine Richtung, in der Regel von der übergeordneten Komponente 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 denen 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.

Unter Berücksichtigung gängiger Architekturprinzipien sollte jede Anwendung mindestens zwei Ebenen haben:

  • 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. Wenn sich die Daten ändern, entweder aufgrund einer Nutzerinteraktion (z. B. durch Drücken einer Schaltfläche) oder durch externe Eingaben (z. B. eine Netzwerkantwort), sollte die Benutzeroberfläche aktualisiert werden, 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.
  • State-Holder (z. B. ViewModel), die Daten enthalten, sie für die Benutzeroberfläche verfügbar machen und die Logik verarbeiten
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-Status bereit, der sich an verschiedene Fenstergrößenklassen anpasst. 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 auf der Seite zum UI-Layer.

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. Sie sollten für jeden Datentyp, den Sie in Ihrer App verarbeiten, eine Repository-Klasse erstellen. Sie können beispielsweise eine MoviesRepository-Klasse für Daten zu Filmen oder eine PaymentsRepository-Klasse für Daten zu 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 sollte nur für die Verarbeitung einer Datenquelle zuständig sein. 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 Domain-Schicht ist dafür verantwortlich, komplexe Geschäftslogik oder einfachere Geschäftslogik, die von mehreren Viewmodels wiederverwendet wird, zu kapseln. Die Domänenebene ist optional, da nicht alle Apps diese Anforderungen erfüllen. Verwenden Sie sie nur, wenn es erforderlich ist, 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 sollte für eine einzelne Funktion verantwortlich sein. 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 Domainebene.

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 Komplexität hinzugefügt wird. Außerdem können Sie mit den Mustern schnell zwischen Test- und Produktionsimplementierungen wechseln.

Allgemeine Best Practices

Programmieren ist ein kreatives Feld und die Entwicklung 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. Die Einstiegspunkte sollten nur mit anderen Komponenten zusammenarbeiten, um die für diesen Einstiegspunkt relevante Teilmenge der Daten abzurufen. 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:

Ihre App-Komponenten sollten die einzigen Klassen sein, die auf SDK-APIs des Android-Frameworks 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 wie Daten-Caching und Datenbindung in derselben Klasse. Die empfohlene App-Architektur kann hier helfen.

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 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 Ihre Zeit und Energie 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 bei 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 adaptiven 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 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 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 aus dem Hauptthread 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 der App, zu skalieren. 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.
  • Einfacher zu testen. Eine gute Architektur fördert einfachere Typen, die in der Regel leichter zu testen sind.
  • Fehler können methodisch mit klar definierten Prozessen untersucht werden.

Investitionen in die Architektur haben auch direkte Auswirkungen auf die Nutzer. Sie profitieren von einer stabileren Anwendung und mehr Funktionen, da das Entwicklerteam produktiver ist. Die Architektur erfordert jedoch auch einen Zeitaufwand im Vorfeld. Damit Sie die Zeit, die Sie dafür benötigen, gegenüber dem Rest Ihres Unternehmens rechtfertigen können, sollten Sie sich diese Fallstudien ansehen. Darin berichten andere Unternehmen von ihren Erfolgen mit einer guten Architektur in ihrer App.

Produktproben

Die folgenden Beispiele veranschaulichen eine gute App-Architektur: