Die App-Architektur ist die Grundlage einer hochwertigen 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, 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, 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. Das 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 der einzelnen Teile.
Aufteilung der Zuständigkeiten
Die App-Architektur sollte einigen bestimmten Prinzipien folgen.
Das wichtigste Prinzip 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 von Activity oder Fragment besteht darin, die Benutzeroberfläche Ihrer App zu hosten. Der Lebenszyklus von Ansichten wird vom Android-Betriebssystem gesteuert. Sie werden häufig als Reaktion auf Nutzeraktionen wie die Bildschirmrotation oder Systemereignisse wie wenig Speicherplatz 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 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.
UI aus Datenmodellen erstellen
Ein weiterer wichtiger Grundsatz ist, dass Sie die Benutzeroberfläche aus Datenmodellen ableiten sollten, 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.
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.
Single Source of Truth
Wenn in Ihrer App ein neuer Datentyp definiert wird, sollten Sie ihm eine einzige „Source of Truth“ (SSOT) zuweisen. 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 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 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 Umfang in der Hierarchie zu denen mit niedrigerem Umfang. 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.
Empfohlene App-Architektur
Gemäß den gängigen 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.
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 State-Holdern zur Verwaltung der Komplexität der UI
- 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-Ebene (oder Darstellungsebene) 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, mit denen die Daten auf dem Bildschirm gerendert werden. 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 verfügbar machen und die Logik verarbeiten
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 basierend auf dem verfügbaren Bildschirmplatz automatisch zwischen verschiedenen Navigationsmustern (z. B. NavigationBar, NavigationRail oder NavigationDrawer) zu wechseln.
Weitere Informationen finden Sie auf der Seite zur UI-Ebene.
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.
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.
Domänenebene
Die Domänenebene ist eine optionale Ebene zwischen der UI- und der Datenebene.
Die Domänenschicht ist dafür verantwortlich, komplexe Geschäftslogik oder einfachere Geschäftslogik zu kapseln, die von mehreren Ansichtsmodellen wiederverwendet wird. 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.
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 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 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 das Erstellen von Android-Apps ist 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 koordiniert werden, um 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:
Ihre App-Komponenten sollten die einzigen Klassen sein, die 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 wie Daten-Caching und Datenbindung in derselben Klasse. Die empfohlene App-Architektur kann dabei 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 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 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 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 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 Wartungsfreundlichkeit, 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 mithilfe von klar definierten Prozessen methodisch 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: