Leitfaden zur App-Architektur

Dieser Leitfaden enthält Best Practices und eine empfohlene Architektur für die Entwicklung robuster, hochwertiger Apps.

User Experience bei mobilen Apps

Eine typische Android-App enthält mehrere App-Komponenten, darunter Aktivitäten, Fragmente, Dienste, Contentanbieter und Broadcast-Empfänger. Die meisten dieser App-Komponenten deklarieren Sie in Ihrem App-Manifest. Das Android-Betriebssystem verwendet diese Datei dann, um zu entscheiden, wie Ihre App in die allgemeine Nutzererfahrung des Geräts integriert werden soll. Da eine typische Android-App mehrere Komponenten enthalten kann und Nutzer oft in kurzer Zeit mit mehreren Apps interagieren, müssen sich Apps an verschiedene nutzergesteuerte Workflows und Aufgaben anpassen.

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. Eine App kann nicht davon ausgehen, dass sie im Hoch- oder Querformat ausgeführt wird. Sie kann in einer einzelnen Sitzung im Hoch- oder Querformat oder in beiden Formaten ausgeführt werden. Konfigurationsänderungen, z. B. wenn ein Nutzer ein faltbares Gerät in den Tablet- oder Buchmodus ändert, können dazu führen, dass die Benutzeroberfläche Ihrer App neu zusammengesetzt wird. Dies kann sich auf App-Daten und den App-Status auswirken.

Ressourcenbeschränkungen

Beachten Sie, dass auch Mobilgeräte ressourcenbeschränkt sind. Das Betriebssystem kann daher jederzeit einige App-Prozesse beenden, um Platz für neue zu schaffen.

Bedingungen für die Einführung von Variablen

Unter diesen Bedingungen können Ihre App-Komponenten einzeln und in beliebiger Reihenfolge gestartet werden und das Betriebssystem oder der Nutzer kann sie jederzeit beenden. Da diese Ereignisse nicht von Ihnen gesteuert werden, sollten Sie keine Anwendungsdaten oder ‑status in den App-Komponenten speichern oder im Arbeitsspeicher behalten. Außerdem sollten die App-Komponenten nicht voneinander abhängig sein.

Allgemeine Architekturprinzipien

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

Da Android-Apps immer größer werden, ist es wichtig, eine Architektur zu definieren, die es ermöglicht, die App zu skalieren, an verschiedene Formen und Größen anzupassen, die Robustheit der App zu erhöhen und die App einfacher zu testen.

Eine App-Architektur definiert die Grenzen zwischen den einzelnen Teilen der App und die Verantwortlichkeiten der einzelnen Teile. Damit Ihre App allen Richtlinien entspricht, sollten Sie die App-Architektur nach einigen bestimmten Prinzipien ausrichten.

Aufteilung der Zuständigkeiten

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. Diese UI-basierten Klassen sollten nur Logik enthalten, die UI- und Betriebssysteminteraktionen verarbeitet. Wenn Sie diese Klassen so schlank wie möglich halten, können Sie viele Probleme im Zusammenhang mit dem Komponentenlebenszyklus vermeiden und die Testbarkeit dieser Klassen verbessern.

Denken Sie daran, dass Sie keine Implementierungen von Activity und Fragment besitzen. Das sind nur Glue-Klassen, die den Vertrag zwischen dem Android-Betriebssystem und Ihrer App darstellen. Das Betriebssystem kann sie jederzeit aufgrund von Nutzerinteraktionen oder aufgrund von Systembedingungen wie wenig Arbeitsspeicher zerstören. Um eine zufriedenstellende Nutzererfahrung und eine einfachere App-Wartung zu ermöglichen, sollten Sie die Abhängigkeit von ihnen minimieren.

Adaptive Layouts

Ihre App sollte Konfigurationsänderungen problemlos verarbeiten können, z. B. wenn der Nutzer zwischen Hoch- und Querformat wechselt oder wenn die App auf großen Bildschirmen ausgeführt wird. Eine App, in der die kanonischen adaptiven Layouts implementiert sind, bietet eine optimale Nutzerfreundlichkeit auf einer Vielzahl von Formfaktoren.

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:

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

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

Wenn Sie Ihre App-Architektur auf Datenmodellklassen basieren, wird Ihre App testbarer und robuster.

Single Source of Truth

Wenn in Ihrer App ein neuer Datentyp definiert wird, sollten Sie ihm eine Single 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 verfügbar gemacht. Zum Ändern der Daten werden Funktionen oder empfangene Ereignisse im SSOT verfügbar gemacht, die von anderen Typen aufgerufen werden können.

Dieses Muster bietet mehrere Vorteile:

  • Alle Änderungen an einem bestimmten Datentyp werden an einem Ort zentralisiert.
  • Sie schützt die Daten vor Manipulationen durch andere Typen.
  • Änderungen an den Daten lassen sich so besser nachvollziehen. So lassen sich Fehler leichter erkennen.

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 oder sogar die Benutzeroberfläche sein.

Unidirektionaler Datenfluss

Das Prinzip der zentralen Informationsquelle wird in unseren Anleitungen häufig mit dem Muster für unidirektionalen Datenfluss (Unidirectional Data Flow, UDF) verwendet. In UDF fließt der Status nur in eine Richtung. 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 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.

In diesem Abschnitt wird gezeigt, wie Sie Ihre App gemäß den empfohlenen Best Practices strukturieren.

Unter Berücksichtigung der im vorherigen Abschnitt erwähnten allgemeinen Architekturprinzipien sollte jede Anwendung mindestens zwei Ebenen haben:

  • Die UI-Ebene, auf der Anwendungsdaten auf dem Bildschirm angezeigt werden.
  • Die Datenebene, die die Geschäftslogik Ihrer App enthält und Anwendungsdaten bereitstellt.

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

Die moderne App-Architektur empfiehlt unter anderem die Verwendung der folgenden Techniken:

  • Eine adaptive und mehrschichtige Architektur.
  • Unidirektionaler Datenfluss (UDF) in allen Ebenen der App.
  • Eine UI-Ebene mit State Holdern, um die Komplexität der UI zu verwalten.
  • Coroutinen und Flows.
  • Best Practices für die Abhängigkeitsinjektion

Weitere Informationen finden Sie in den folgenden Abschnitten, auf den anderen Architekturseiten im Inhaltsverzeichnis und auf der Seite mit Empfehlungen, die eine Zusammenfassung der wichtigsten Best Practices enthält.

UI-Ebene

Die Aufgabe der UI-Ebene (oder Darstellungsebene) besteht darin, die Anwendungsdaten auf dem Bildschirm darzustellen. 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 besteht aus zwei Elementen:

  • UI-Elemente, mit denen die Daten auf dem Bildschirm gerendert werden. Sie erstellen diese Elemente mit Ansichten oder Jetpack Compose-Funktionen. Sowohl Views als auch Jetpack Compose unterstützen adaptive Layouts.
  • Statusinhaber (z. B. ViewModel-Klassen), die Daten enthalten, sie für die Benutzeroberfläche bereitstellen und die Logik verarbeiten.
In einer typischen Architektur hängen die UI-Elemente der UI-Ebene von State-Holdern ab, die wiederum von Klassen aus der Daten- oder der optionalen Domänenebene abhängen.
Abbildung 2. Die Rolle der UI-Ebene in der App-Architektur.

Weitere Informationen zu dieser Ebene finden Sie auf der Seite UI-Ebene.

Datenebene

Die Datenschicht einer App enthält die Geschäftslogik. Die Geschäftslogik ist das, was Ihrer App Wert verleiht. Sie besteht aus Regeln, die festlegen, 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 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 die folgenden Aufgaben 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 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 zu dieser Ebene

Domänenebene

Die Domänenschicht ist eine optionale Schicht, die sich zwischen der UI- und der Datenschicht befindet.

Die Domain-Schicht ist für die Kapselung komplexer Geschäftslogik oder einfacherer Geschäftslogik verantwortlich, die von mehreren ViewModels wiederverwendet wird. Diese Ebene ist optional, da nicht alle Apps diese Anforderungen erfüllen müssen. Sie sollten es nur bei Bedarf verwenden, 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 dieser Ebene werden häufig als Anwendungsfälle oder Interaktoren bezeichnet. Jeder Anwendungsfall sollte für eine einzelne Funktion verantwortlich sein. Ihre App könnte beispielsweise die Klasse GetTimeZoneUseCase haben, wenn mehrere ViewModels Zeitzonen benötigen, um die richtige Nachricht auf dem Bildschirm anzuzeigen.

Weitere Informationen zu dieser Ebene

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 selbst 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 diesen 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 Ihre Codebasis dadurch robuster, testbarer und langfristig 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. Stattdessen sollten sie sich nur mit anderen Komponenten abstimmen, um die für diesen Einstiegspunkt relevanten Daten abzurufen. Jede App-Komponente ist relativ kurzlebig, je nachdem, wie der Nutzer mit seinem Gerät interagiert und wie der allgemeine aktuelle Zustand des Systems ist.

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 ihnen abstrahieren, wird die Testbarkeit verbessert und die Kopplung in Ihrer App verringert.

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 Ihrer Codebasis. Definieren Sie auch nicht mehrere unabhängige Verantwortlichkeiten, z. B. Daten-Caching und Datenbindung, in derselben Klasse. Wenn Sie die empfohlene App-Architektur verwenden, wird Ihnen das leichter fallen.

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

Erstellen Sie beispielsweise keine Verknüpfung, die ein internes Implementierungsdetail aus einem Modul verfügbar macht. Kurzfristig sparen Sie vielleicht etwas Zeit, aber im Laufe der Zeit werden Sie wahrscheinlich umso mehr technische Schulden anhäufen, wenn sich Ihre Codebasis weiterentwickelt.

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, und 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 verbessern. Sehen Sie sich die Galerie mit App-Designmustern an, um die Layouts auszuwählen, die für Ihre Anwendungsfälle am besten geeignet sind.

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

Wenn Sie beispielsweise eine gut definierte API zum Abrufen von Daten aus dem Netzwerk haben, ist es einfacher, das Modul zu testen, das diese Daten in einer lokalen Datenbank speichert. Wenn Sie die Logik aus diesen beiden Modulen stattdessen an einem Ort zusammenfassen oder den Netzwerkcode über die gesamte Codebasis verteilen, wird es viel schwieriger, effektiv zu testen.

Typen sind für ihre Richtlinie zur Parallelität verantwortlich.

Wenn ein Typ lang andauernde blockierende Aufgaben ausführt, sollte er dafür sorgen, dass die Berechnung in den richtigen Thread verschoben wird. Dieser spezielle Typ kennt die Art der Berechnung, die er durchführt, und den Thread, in dem er 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 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:

  • Dadurch werden die Wartungsfreundlichkeit, Qualität und Robustheit der gesamten App verbessert.
  • Dadurch kann die App skaliert werden. Mehr Personen und Teams können mit minimalen Codekonflikten zur selben Codebasis beitragen.
  • Sie erleichtert das 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.
  • Fehler können mithilfe von klar definierten Prozessen methodisch untersucht werden.

Investitionen in die Architektur haben auch direkte Auswirkungen auf Ihre Nutzer. Sie profitieren von einer stabileren Anwendung und mehr Funktionen, da das Entwicklerteam produktiver ist. Die Architektur erfordert jedoch auch einen Zeitaufwand im Voraus. Fallstudien können Ihnen helfen, die Zeit, die Sie für die Architektur aufwenden, gegenüber dem Rest des Unternehmens zu rechtfertigen. Darin berichten andere Unternehmen von ihren Erfolgen mit einer guten Architektur in ihrer App.

Produktproben

Die folgenden Google-Beispiele zeigen eine gute App-Architektur. Sehen Sie sich die folgenden Beispiele an, um zu sehen, wie diese Anleitung in der Praxis aussieht: