Leitfaden zur App-Architektur

Dieser Leitfaden enthält Best Practices und empfohlene Architekturen zum Erstellen robuster, hochwertiger Anwendungen.

Nutzererfahrung in 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 Gesamtnutzung des Geräts integriert wird. Da eine typische Android-App mehrere Komponenten enthalten kann und Nutzer häufig innerhalb kurzer Zeit mit mehreren Anwendungen interagieren, müssen sich Anwendungen an verschiedene Arten von nutzergesteuerten Workflows und Aufgaben anpassen.

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

Angesichts der Bedingungen dieser Umgebung ist es möglich, dass Ihre Anwendungskomponenten einzeln und in falscher Reihenfolge gestartet werden und das Betriebssystem oder der Nutzer sie jederzeit zerstören kann. Da diese Ereignisse nicht von Ihnen gesteuert werden, sollten Sie keine Anwendungsdaten oder -status in Ihren Anwendungskomponenten speichern oder im Arbeitsspeicher behalten. Außerdem sollten die Anwendungskomponenten nicht voneinander abhängig sein.

Allgemeine Architekturprinzipien

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

Wenn Android-Apps größer werden, ist es wichtig, eine Architektur zu definieren, mit der die App skaliert, robuster und einfacher getestet werden kann.

In einer Anwendungsarchitektur werden die Grenzen zwischen Teilen der App und die Verantwortlichkeiten definiert, die jeder Teil haben sollte. Um die oben genannten Anforderungen zu erfüllen, sollten Sie bei der Entwicklung Ihrer Anwendungsarchitektur einige spezifische Prinzipien beachten.

Trennung von Anliegen

Das wichtigste Prinzip, das befolgt werden muss, ist die Trennung von Belangen. Es kommt häufig vor, dass der gesamte Code in einem Activity- oder Fragment-Element geschrieben wird. 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.

Beachten Sie, dass Sie keine Implementierungen von Activity und Fragment haben. Es handelt sich vielmehr um 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 löschen. Für eine zufriedenstellende Nutzererfahrung und eine überschaubare Anwendungswartung sollten Sie Ihre Abhängigkeit von diesen Anwendungen minimieren.

Drive-UI aus Datenmodellen

Ein weiteres wichtiges Prinzip ist, dass Sie Ihre UI aus Datenmodellen, vorzugsweise von persistenten Modellen, ableiten sollten. 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 die UI und den Lebenszyklus der App-Komponenten gebunden sind, aber trotzdem gelöscht werden, wenn das Betriebssystem den Prozess der App aus dem Speicher entfernt.

Persistente Modelle sind aus folgenden Gründen ideal:

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

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

Wenn Ihre App-Architektur auf Datenmodellklassen basiert, wird Ihre App testbar und robuster.

Eine Single Source of Truth

Wenn in Ihrer Anwendung ein neuer Datentyp definiert wird, sollten Sie ihm eine Single Source of Truth (SSOT) zuweisen. Die SSOT ist der Inhaber dieser Daten und nur die SSOT kann sie ändern. Zu diesem Zweck stellt SSOT die Daten in einem unveränderlichen Typ zur Verfügung. Um die Daten zu ändern, stellt SSOT Funktionen bereit oder empfängt Ereignisse, die andere Typen aufrufen können.

Dieses Muster bietet mehrere Vorteile:

  • Es fasst alle Änderungen an einem bestimmten Datentyp an einem Ort zusammen.
  • Die Daten werden so geschützt, dass andere Typen sie nicht manipulieren können.
  • Sie macht Änderungen an den Daten nachvollziehbarer. Daher sind Fehler leichter zu erkennen.

In einer Offline-First-Anwendung ist die „Source of Truth“ für Anwendungsdaten in der Regel eine Datenbank. In einigen anderen Fällen kann die zentrale Datenquelle ein ViewModel oder sogar die UI sein.

Unidirektionaler Datenfluss

Das Single Source of Truth-Prinzip wird in unseren Leitfäden mit dem Unidirektionalen Datenfluss-Muster (UDF) häufig verwendet. Bei UDF fließt der state nur in eine Richtung. Die Ereignisse, die den Datenfluss in die entgegengesetzte Richtung ändern.

In Android fließen Status oder Daten in der Regel von den übergeordneten Typen der Hierarchie zu den niedrigeren. Ereignisse werden normalerweise von den untergeordneten Typen ausgelöst, bis sie die SSOT für den entsprechenden Datentyp erreichen. Anwendungsdaten werden beispielsweise in der Regel von Datenquellen zur UI übertragen. Nutzerereignisse wie das Drücken von Schaltflächen werden von der UI zur SSOT weitergeleitet, wo die Anwendungsdaten geändert und in einem unveränderlichen Typ verfügbar gemacht werden.

Dieses Muster garantiert die Datenkonsistenz besser, ist weniger anfällig für Fehler, ist einfacher zu debuggen und bietet alle Vorteile des SSOT-Musters.

In diesem Abschnitt wird gezeigt, wie Sie Ihre Anwendung anhand der empfohlenen Best Practices strukturieren.

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

  • Die UI-Ebene, die Anwendungsdaten auf dem Bildschirm anzeigt.
  • Die Datenschicht, die die Geschäftslogik Ihrer Anwendung enthält und Anwendungsdaten bereitstellt.

Sie können eine zusätzliche Ebene, die Domainebene, hinzufügen, um die Interaktionen zwischen der UI und den Datenschichten zu vereinfachen und wiederzuverwenden.

In einer typischen Anwendungsarchitektur erhält die UI-Ebene die Anwendungsdaten aus der Datenschicht oder von der optionalen Domainebene, die sich zwischen der UI-Ebene und der Datenschicht befindet.
Abbildung 1. Diagramm einer typischen Anwendungsarchitektur.

Moderne Anwendungsarchitektur

Diese moderne Anwendungsarchitektur unterstützt unter anderem die folgenden Techniken:

  • Eine reaktive und mehrschichtige Architektur.
  • Unidirektionaler Datenfluss (UDF) in allen Ebenen der Anwendung
  • Eine UI-Ebene mit Statusinhabern zur Verwaltung der Komplexität der UI.
  • Koroutinen und Abläufe.
  • Best Practices für Abhängigkeitsinjektionen.

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

UI-Ebene

Die UI-Ebene (oder Darstellungsebene) hat die Aufgabe, die Anwendungsdaten auf dem Bildschirm anzuzeigen. Wenn sich die Daten aufgrund einer Nutzerinteraktion (z. B. Drücken einer Schaltfläche) oder externer Eingaben (z. B. einer Netzwerkantwort) ändern, 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 mithilfe von Ansichten oder Jetpack Compose-Funktionen.
  • Statusinhaber (z. B. ViewModel-Klassen), die Daten enthalten, für die UI verfügbar machen und die Logik verarbeiten.
In einer typischen Architektur hängen die UI-Elemente der UI-Ebene von Statusinhabern ab, die wiederum von Klassen aus der Datenschicht oder der optionalen Domainebene abhängen.
Abbildung 2. Die Rolle der UI-Ebene in der App-Architektur.

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

Data-Ebene

Die Datenschicht einer App enthält die Geschäftslogik. Die Geschäftslogik ist das, was Ihrer Anwendung einen Mehrwert verleiht. Sie besteht aus Regeln, die bestimmen, wie Ihre Anwendung 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 Anwendung verarbeiten, eine Repository-Klasse erstellen. Beispielsweise könnten Sie eine MoviesRepository-Klasse für Daten zu 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 Anwendung bereit und sind von den Datenquellen abhängig.
Abbildung 3. Die Rolle der Datenschicht in der Anwendungsarchitektur.

Repository-Klassen sind für die folgenden Aufgaben verantwortlich:

  • Daten werden für den Rest der App freigegeben.
  • Änderungen an den Daten zentralisieren.
  • Konflikte zwischen mehreren Datenquellen beheben
  • Abstraktion von Datenquellen aus dem Rest der App.
  • Enthält Geschäftslogik.

Jede Datenquellenklasse sollte nur mit einer Datenquelle arbeiten. Dies kann eine Datei, eine Netzwerkquelle oder eine lokale Datenbank sein. Datenquellenklassen sind das Bindeglied zwischen der Anwendung und dem System für Datenvorgänge.

Weitere Informationen zu dieser Ebene finden Sie auf der Seite „Datenschicht“.

Domainebene

Die Domainebene ist eine optionale Ebene, die sich zwischen der UI und den Datenebenen befindet.

Die Domainebene ist für die Kapselung komplexer Geschäftslogik bzw. einfacher Geschäftslogik verantwortlich, die von mehreren ViewModels wiederverwendet wird. Diese Ebene ist optional, da nicht alle Anwendungen diese Anforderungen erfüllen. Sie sollten sie nur bei Bedarf verwenden, z. B. um Komplexität zu bewältigen oder die Wiederverwendbarkeit zu bevorzugen.

Die optionale Domainebene stellt Abhängigkeiten für die UI-Ebene bereit und hängt von der Datenschicht ab.
Abbildung 4: Die Rolle der Domainebene in der Anwendungsarchitektur.

Klassen in dieser Ebene werden allgemein als Anwendungsfälle oder Interaktionen bezeichnet. Jeder Anwendungsfall sollte die Verantwortung für eine einzelne Funktionalität haben. 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 finden Sie auf der Seite mit der Domainebene.

Abhängigkeiten zwischen Komponenten verwalten

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

  • Abhängigkeitsinjektion (DI): Mithilfe der Abhängigkeitsinjektion 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 den Code skalieren, da sie klare Muster für die Verwaltung von Abhängigkeiten enthalten, ohne Code zu duplizieren oder die Komplexität zu erhöhen. Außerdem können Sie mit diesen Mustern schnell zwischen Test- und Produktionsimplementierungen wechseln.

Wir empfehlen, Muster für Abhängigkeitsinjektionen zu befolgen und die Hilt-Bibliothek in Android-Apps zu verwenden. Hilt erstellt automatisch Objekte durch Durchlaufen des Abhängigkeitsbaums, bietet Garantien für die Kompilierungszeit für Abhängigkeiten und erstellt Abhängigkeitscontainer für Android-Framework-Klassen.

Allgemeine Best Practices

Die Programmierung ist ein kreatives Feld und die Entwicklung 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 kommunizieren, Remote-Daten abrufen und lokal für den Offline-Modus speichern oder eine Reihe anderer gängiger Szenarien bewältigen, die bei nicht trivialen Anwendungen auftreten.

Obwohl die folgenden Empfehlungen nicht obligatorisch sind, wird Ihre Codebasis in den meisten Fällen langfristig robuster, testbar und wartbar:

Speichern Sie keine Daten in App-Komponenten.

Legen Sie Einstiegspunkte Ihrer Anwendung wie Aktivitäten, Dienste und Sendeempfänger nicht als Datenquellen fest. Stattdessen sollten sie sich nur mit anderen Komponenten abstimmen, um die Teilmenge der Daten abzurufen, die für diesen Einstiegspunkt relevant sind. Jede App-Komponente ist recht kurzlebig, abhängig von der Interaktion des Nutzers mit seinem Gerät und dem aktuellen Gesamtzustand des Systems.

Reduzieren Sie die Abhängigkeiten von Android-Klassen.

Ihre App-Komponenten sollten die einzigen Klassen sein, die auf Android Framework SDK APIs wie Context oder Toast basieren. Wenn Sie andere Klassen in der Anwendung abziehen, verbessern Sie die Testbarkeit und reduzieren die Kopplung innerhalb Ihrer Anwendung.

Definieren Sie klar definierte Verantwortungsgrenzen zwischen verschiedenen Modulen in Ihrer Anwendung.

Verteilen Sie beispielsweise den Code zum Laden von Daten aus dem Netzwerk nicht auf mehrere Klassen oder Pakete in Ihrer Codebasis. Ebenso sollten Sie nicht mehrere unzusammenhängende Verantwortlichkeiten (z. B. Daten-Caching und Datenbindung) in derselben Klasse definieren. Dabei hilft Ihnen die empfohlene Anwendungsarchitektur.

Stellen Sie möglichst wenig aus jedem Modul heraus.

Sie sollten beispielsweise nicht versucht sein, eine Verknüpfung zu erstellen, mit der ein internes Implementierungsdetail aus einem Modul offengelegt wird. Kurzfristig mag es etwas Zeit bringen, aber dann gehen technische Schulden mit der Weiterentwicklung Ihrer Codebasis um ein Vielfaches auf.

Konzentriere dich auf das Wesentliche deiner App, damit sie sich von anderen Apps abhebt.

Sie erfinden das Rad nicht neu, indem Sie immer wieder denselben Boilerplate-Code schreiben. Konzentrieren Sie sich stattdessen auf das, was Ihre App so einzigartig macht, und überlassen Sie die Jetpack-Bibliotheken und andere empfohlene Bibliotheken den sich wiederholenden Standardcode.

Überlege dir, wie du jeden Teil deiner App isoliert testbar machst.

Mit einer klar definierten API zum Abrufen von Daten aus dem Netzwerk ist es beispielsweise einfacher, das Modul zu testen, das diese Daten in einer lokalen Datenbank beibehält. Wenn Sie stattdessen die Logik dieser beiden Module an einem Ort mischen oder Ihren Netzwerkcode über Ihre gesamte Codebasis verteilen, wird es viel schwieriger – wenn nicht sogar unmöglich –, effektiv zu testen.

Typen sind für ihre Nebenläufigkeitsrichtlinie verantwortlich.

Wenn ein Typ Blockierarbeit mit langer Ausführungszeit ausführt, sollte er dafür verantwortlich sein, diese Berechnung in den richtigen Thread zu verschieben. Dieser spezielle Typ kennt die Art der Berechnung, die er durchführt, und in welchem Thread er ausgeführt werden soll. Typen sollten „hauptsicher“ sein, d. h. sie können sicher aus dem Hauptthread aufgerufen werden, ohne ihn zu blockieren.

Sorgen Sie dafür, dass möglichst viele relevante und aktuelle Daten vorliegen.

So können Nutzer die Funktionen Ihrer App auch dann nutzen, wenn ihr Gerät offline ist. Denken Sie daran, dass nicht alle Nutzer eine ständige Hochgeschwindigkeitsverbindung nutzen – und selbst wenn sie dies tun, können sie an überfüllten Orten einen schlechten Empfang haben.

Vorteile der Architektur

Eine gute Architektur in Ihrer Anwendung zu implementieren bietet den Projekt- und Entwicklungsteams viele Vorteile:

  • Sie verbessert die Wartbarkeit, Qualität und Robustheit der gesamten App.
  • Sie ermöglicht die Skalierung der App. Bei minimalen Codekonflikten können mehr Personen und mehr Teams zur selben Codebasis beitragen.
  • Das erleichtert das Onboarding. Wenn die Architektur für Einheitlichkeit in Ihrem Projekt sorgt, können sich neue Teammitglieder schnell einarbeiten und in kürzerer Zeit effizienter arbeiten.
  • Es ist einfacher zu testen. Eine gute Architektur unterstützt einfachere Typen, die im Allgemeinen einfacher zu testen sind.
  • Fehler können methodisch mit klar definierten Prozessen untersucht werden.

Investitionen in Architektur wirken sich auch direkt auf Ihre Nutzer aus. Sie profitieren von einer stabileren Anwendung und mehr Funktionen aufgrund eines produktiveren Engineering-Teams. Die Architektur erfordert jedoch auch Vorabinvestitionen. Damit Sie diese Zeit dem Rest Ihres Unternehmens gegenüber begründen können, sollten Sie sich diese Fallstudien ansehen, in denen andere Unternehmen von einer guten Architektur in ihren Apps berichten.

Produktproben

Die folgenden Google-Beispiele veranschaulichen eine gute Anwendungsarchitektur. Sehen Sie sich diese an und sehen Sie sich diese Anleitung in der Praxis an: