Es gibt keine einzelne Modularisierungsstrategie, die für alle Projekte geeignet ist. Aufgrund von Da Gradle flexibel ist, gibt es nur wenige Einschränkungen um ein Projekt zu organisieren. Diese Seite bietet einen Überblick über einige allgemeine Regeln und häufige Mustern, die du bei der Entwicklung mehrteiliger Android-Apps nutzen kannst.
Prinzip der hohen Kohäsion und niedriger Kopplung
Eine Möglichkeit zur Charakterisierung einer modularen Codebasis wäre die Verwendung der Kopplung und cohesion-Eigenschaften. Die Kopplung misst den Grad, in dem Module voneinander abhängig sind. Kohäsion misst in diesem Kontext, wie die Elemente einer einzelnen Modulen zusammenhängen. In der Regel sollten Sie geringe Kopplung und hohe Kohäsion:
- Geringe Kopplung bedeutet, dass die Module so unabhängig wie möglich von den sodass Änderungen an einem Modul keine oder nur minimale Auswirkungen anderen Modulen. Module sollten keine Kenntnisse über das Innenleben eines anderen Modulen.
- Hohe Kohäsion bedeutet, dass Module eine Sammlung von Code enthalten sollten, agiert als System. Sie sollten klar definierte Verantwortlichkeiten haben und innerhalb der Grenzen eines bestimmten Domänenwissens liegen. Beispiel-E-Book ansehen . Es könnte unangemessen sein, Code für Buch und Zahlung zu mischen im selben Modul, da es sich um unterschiedliche Funktionsbereiche handelt.
Modultypen
Die Art und Weise, wie Sie Ihre Module organisieren, hängt hauptsächlich von Ihrer Anwendungsarchitektur ab. Darunter sind einige gängige Modultypen, die Sie in Ihrer App einführen können, unserer empfohlenen Anwendungsarchitektur.
Datenmodule
Ein Datenmodul enthält normalerweise ein Repository, Datenquellen und Modellklassen. Die drei Hauptaufgaben eines Datenmoduls sind:
- Alle Daten und Geschäftslogik einer bestimmten Domain verkapseln: Alle Daten -Moduls verantwortlich für die Verarbeitung von Daten sein, . Es kann viele Datentypen verarbeiten, solange sie zusammenhängen.
- Repository als externe API verfügbar machen: Die öffentliche API von Daten. sollte ein Repository sein, da sie dafür verantwortlich sind, für den Rest der App.
- So blenden Sie alle Implementierungsdetails und Datenquellen aus:
Datenquellen sollten nur für Repositories aus demselben Modul zugänglich sein.
Sie bleiben nach außen verborgen. Sie können dies erzwingen, indem Sie die
private
oderinternal
Sichtbarkeits-Keyword.
Funktionsmodule
Eine Funktion ist ein isolierter Teil der Funktionalität einer App, der in der Regel auf einem Bildschirm oder einer Reihe von eng verwandten Bildschirmen, z. B. Anmeldungen oder Bezahlvorgänge, Ablauf. Verfügt Ihre App über eine Leiste am unteren Rand, ist eine Funktion.
<ph type="x-smartling-placeholder">Die Funktionen sind mit Bildschirmen oder Zielen in Ihrer App verknüpft. Dementsprechend wird
haben sie wahrscheinlich eine zugehörige Benutzeroberfläche und ViewModel
, um ihre Logik zu verarbeiten.
und Bundesstaat. Ein einzelnes Feature muss nicht auf eine einzelne Ansicht oder
Navigationsziel. Funktionsmodule hängen von Datenmodulen ab.
App-Module
App-Module sind ein Einstiegspunkt für die Anwendung. Sie hängen von der Funktion ab, Module und bieten normalerweise Root-Navigation. Ein einzelnes App-Modul kann kompiliert werden mithilfe von Build-Varianten in verschiedene Binärprogramme umwandeln.
<ph type="x-smartling-placeholder">Wenn Ihre App auf mehrere Gerätetypen ausgerichtet ist, etwa „Auto“, „Wear“ oder „TV“, definieren Sie für jeden Gerätetyp ein App-Modul. So lassen sich plattformspezifische Abhängigkeiten.
<ph type="x-smartling-placeholder">Gängige Module
Gängige Module, auch als Kernmodule bezeichnet, enthalten Code, den andere Module die Sie häufig verwenden. Sie reduzieren die Redundanz und stellen keine bestimmte Schicht in die Architektur einer App. Im Folgenden finden Sie Beispiele für gängige Module:
- UI-Modul: Wenn Sie benutzerdefinierte UI-Elemente oder ein ausgeklügeltes Branding in Ihren App ist, sollten Sie erwägen, Ihre Widget-Sammlung in einem Modul wiederverwendbar. Dies könnte dazu beitragen, Ihre UI auf allen unterschiedliche Funktionen. Sind Ihre Themen beispielsweise zentralisiert, können Sie vermeiden, bei einem Rebranding eine schmerzhafte Refaktorierung ist.
- Analytics-Modul: Tracking wird oft durch Geschäftsanforderungen bestimmt. für die Softwarearchitektur wenig berücksichtigen. Analytics-Tracker sind die oft in vielen zusammenhängenden Komponenten verwendet werden. In diesem Fall kann es sein, empfiehlt es sich, ein eigenes Analytics-Modul zu haben.
- Netzwerkmodul: Wenn viele Module eine Netzwerkverbindung erfordern, können Sie haben Sie die Möglichkeit, ein Modul speziell für die Bereitstellung eines HTTP-Clients zu verwenden. Es ist besonders nützlich, wenn Ihr Client eine benutzerdefinierte Konfiguration benötigt.
- Dienstprogrammmodul: Dienstprogramme, auch als Hilfsprogramme bezeichnet, sind in der Regel kleine Teile. die in der gesamten Anwendung wiederverwendet werden. Beispiele für Dienstprogramme: eine Funktion zum Formatieren von Währungen, eine E-Mail-Validierung oder .
Testmodule
Testmodule sind Android-Module, die nur zu Testzwecken verwendet werden. Die Module enthalten Testcode, Testressourcen und Testabhängigkeiten, die nur sind zum Ausführen von Tests erforderlich und werden während der Laufzeit der Anwendung nicht benötigt. Testmodule werden erstellt, um testspezifischen Code vom Hauptcode zu trennen -Anwendung, wodurch der Modulcode einfacher zu verwalten und zu pflegen ist.
Anwendungsfälle für Testmodule
Die folgenden Beispiele zeigen Situationen, in denen Testmodule kann besonders vorteilhaft sein:
Gemeinsam genutzter Testcode: Wenn Ihr Projekt mehrere Module und einige der Testcode auf mehr als ein Modul anwendbar ist, können Sie um den Code zu teilen. So lassen sich Duplikate vermeiden und der Test einfacher zu verwalten. Ein gemeinsamer Testcode kann Dienstprogrammklassen oder Funktionen wie benutzerdefinierte Assertions oder Matcher sowie Testdaten wie simulierten JSON-Antworten.
Sauberere Build-Konfigurationen: Testmodule ermöglichen eine sauberere Build-Konfigurationen erstellen, da sie eine eigene
build.gradle
-Datei haben können. Das solltest du nicht tun die Dateibuild.gradle
deines App-Moduls mit Konfigurationen, nur für Tests relevant.Integrationstests: Testmodule können zum Speichern der Integration verwendet werden. Tests, mit denen Interaktionen zwischen verschiedenen Teilen der App getestet werden. einschließlich Benutzerschnittstelle, Geschäftslogik, Netzwerkanfragen und Datenbankabfragen.
Umfangreiche Anwendungen: Testmodule sind besonders nützlich für umfangreichen Anwendungen mit komplexen Codebasen und mehreren Modulen. In solchen Fällen können Testmodule dazu beitragen, die Organisation und Verwaltbarkeit des Codes zu verbessern.
Kommunikation zwischen Modulen
Module sind selten vollständig getrennt und stützen sich oft auf andere Module und mit ihnen kommunizieren können. Es ist wichtig, die Kopplung gering zu halten, auch wenn Module zusammenarbeiten und Informationen austauschen. Manchmal direkt Kommunikation zwischen zwei Modulen ist entweder nicht wünschenswert, wie im Fall von Architektureinschränkungen. Möglicherweise ist es unmöglich, wie z. B. bei zyklischen Abhängigkeiten.
<ph type="x-smartling-placeholder">Um dieses Problem zu lösen, kann ein drittes Modul die Vermittlung zwischen zwei anderen Modulen. Das Vermittlermodul kann Nachrichten von beiden und leiten Sie sie bei Bedarf weiter. In unserer Beispiel-App Bildschirm muss wissen, welches Buch gekauft werden soll, separaten Bildschirm, der Teil einer anderen Funktion ist. In diesem Fall „Mediator“ ist das Modul mit dem Navigationsdiagramm (in der Regel ein App-Modul). In diesem Beispiel verwenden wir die Navigation, um die Daten von der Home-Funktion an die der Direktkauf-Funktion mithilfe der Komponente Navigation.
navController.navigate("checkout/$bookId")
Das Checkout-Ziel erhält eine Buch-ID als Argument, die es verwendet, um
Informationen über das Buch abzurufen. Mit dem Ziehpunkt für den gespeicherten Status können Sie
Navigationsargumente im ViewModel
eines Zielelements abrufen.
class CheckoutViewModel(savedStateHandle: SavedStateHandle, …) : ViewModel() {
val uiState: StateFlow<CheckoutUiState> =
savedStateHandle.getStateFlow<String>("bookId", "").map { bookId ->
// produce UI state calling bookRepository.getBook(bookId)
}
…
}
Sie sollten Objekte nicht als Navigationsargumente übergeben. Verwenden Sie stattdessen einfache IDs. mit denen Funktionen auf die gewünschten Ressourcen aus der Datenschicht zugreifen und diese laden können. So halten Sie die Kopplung gering und verletzen nicht die Single Source of Truth Prinzip.
Im folgenden Beispiel hängen beide Funktionsmodule von demselben Datenmodul ab. Dieses ermöglicht, die Datenmenge zu minimieren, die das Vermittlermodul benötigt um die Kopplung zwischen den Modulen gering zu halten. Anstelle von sollten Module primitive IDs austauschen und die Ressourcen aus einer Modul für freigegebene Daten.
<ph type="x-smartling-placeholder">Abhängigkeitsumkehr
Eine Abhängigkeitsumkehr liegt vor, wenn Sie Ihren Code so organisieren, dass die Abstraktion getrennt von einer konkreten Implementierung.
- Abstraktion: Ein Vertrag, der festlegt, wie Komponenten oder Module Anwendungen miteinander interagieren. Abstraktionsmodule definieren die API von Ihres Systems und enthalten Schnittstellen und Modelle.
- Konkrete Implementierung: Module, die vom Abstraktionsmodul abhängen und das Verhalten einer Abstraktion zu implementieren.
Module, die auf dem im Abstraktionsmodul definierten Verhalten basieren, sollten nur hängen von der Abstraktion selbst ab, nicht von den spezifischen Implementierungen.
<ph type="x-smartling-placeholder">Beispiel
Stellen Sie sich ein Funktionsmodul vor, für das eine Datenbank benötigt wird. Das Funktionsmodul ist wie die Datenbank implementiert wird, sei es eine lokale Raumdatenbank oder eine Datenbank Remote-Firestore-Instanz. Er muss nur die Anwendungsdaten speichern und lesen.
Dazu ist das Funktionsmodul auf das Abstraktionsmodul angewiesen, als bei einer bestimmten Datenbankimplementierung. Diese Abstraktion definiert die Database API verwenden können. Mit anderen Worten, es legt die Regeln für die Interaktion mit der Datenbank. Dadurch kann das Funktionsmodul jede Datenbank verwenden, die zugrunde liegenden Implementierungsdetails kennen.
Das konkrete Implementierungsmodul liefert die eigentliche Implementierung des Im Abstraktionsmodul definierte APIs Dazu muss die Implementierung hängt auch vom Abstraktionsmodul ab.
Abhängigkeitsinjektion
Sie fragen sich jetzt vielleicht, wie das Funktionsmodul mit dem Implementierungsmodul. Die Antwort lautet Dependency Injection. Die Funktion die erforderliche Datenbankinstanz nicht direkt erstellt. Stattdessen werden sie gibt an, welche Abhängigkeiten benötigt werden. Diese Abhängigkeiten werden dann bereitgestellt, extern, in der Regel im App-Modul.
releaseImplementation(project(":database:impl:firestore"))
debugImplementation(project(":database:impl:room"))
androidTestImplementation(project(":database:impl:mock"))
Vorteile
Die Trennung Ihrer APIs und ihrer Implementierungen bietet folgende Vorteile:
- Austauschbarkeit: Mit klarer Trennung von API und Implementierung können Sie mehrere Implementierungen für dieselbe API entwickeln und ohne den Code zu ändern, der die API verwendet. Dabei kann es sich um besonders nützlich, wenn Sie unterschiedliche Fähigkeiten oder Verhalten in verschiedenen Kontexten. Beispiel: Ein simulierter für Tests und eine echte Implementierung für die Produktion.
- Entkopplung: Die Trennung bedeutet, dass Module, die Abstraktionen verwenden, von einer bestimmten Technologie abhängen. Wenn Sie Ihre Datenbank von noch Raum für Firestore wäre, wäre es einfacher, da die Änderungen nur in dem jeweiligen Modul für die Aufgabe (Implementierungsmodul) stattfinden mit der API Ihrer Datenbank andere Module beeinflussen.
- Testbarkeit: Die Trennung von APIs von ihren Implementierungen kann erheblich sein. das Testen erleichtern. Sie können Testfälle für die API-Verträge schreiben. Sie können verschiedene Implementierungen verwenden, um verschiedene Szenarien und Grenzfälle zu testen, einschließlich simulierter Implementierungen.
- Verbesserte Build-Leistung: Wenn Sie eine API und ihre die Implementierung in verschiedene Module, das Build-System nicht zwingen, die Module neu zu kompilieren, API-Modul. Dies führt zu kürzeren Build-Daueren und erhöhter Produktivität, insbesondere bei großen Projekten, bei denen die Build-Dauer sehr lange dauern kann.
Wann trennen
Es ist sinnvoll, Ihre APIs von ihren Implementierungen in der folgenden Fällen:
- Vielfältige Funktionen: Wenn Sie Teile Ihres Systems ermöglicht eine klare API die Austauschbarkeit verschiedener Implementierungen. Angenommen, Sie haben ein Rendering-System, das OpenGL oder Vulkan oder einem Abrechnungssystem, das mit Google Play oder Ihrer internen Abrechnung funktioniert der API erstellen.
- Mehrere Anwendungen: Wenn Sie mehrere Anwendungen mit gemeinsame Funktionen für verschiedene Plattformen nutzen, können Sie spezifische Implementierungen für jede Plattform entwickeln.
- Unabhängige Teams: Durch die Trennung können verschiedene Entwickler oder Teams gleichzeitig an verschiedenen Teilen der Codebasis arbeiten. Entwickler sollten sich auf zu den API-Verträgen und ihrer korrekten Verwendung. Sie müssen nicht die Implementierungsdetails anderer Module.
- Große Codebasis: Wenn die Codebasis groß oder komplex ist und die API voneinander trennt. der Implementierung den Code überschaubarer machen. So können Sie die in detailliertere, verständliche und wartbare Einheiten unterteilt.
Implementierung
So implementieren Sie die Abhängigkeitsumkehr:
- Abstraktionsmodul erstellen: Dieses Modul sollte APIs (Schnittstellen) enthalten. und Modelle), die das Verhalten Ihrer Funktion definieren.
- Implementierungsmodule erstellen: Implementierungsmodule sollten auf dem API-Modul und implementieren das Verhalten einer Abstraktion. <ph type="x-smartling-placeholder">
- Machen Sie übergeordnete Module von Abstraktionsmodulen abhängig: Anstelle von direkt von einer spezifischen Implementierung abhängig machen, Abstraktionsmodule. Für übergeordnete Module ist keine Kenntnis der Implementierung erforderlich. benötigen sie nur den Vertrag (API). <ph type="x-smartling-placeholder">
- Implementierungsmodul bereitstellen: Abschließend müssen Sie das Implementierung für Ihre Abhängigkeiten. Die jeweilige Implementierung hängt davon ab, In der Regel ist das App-Modul dafür aber gut geeignet. Um die Implementierung bereitzustellen, geben Sie sie als Abhängigkeit für die von Ihnen ausgewählte eine Variante oder einen Testquellensatz erstellen. <ph type="x-smartling-placeholder">
Allgemeine Best Practices
Wie anfangs erwähnt wird, gibt es nicht nur einen einzigen richtigen Weg, App mit mehreren Modulen. So wie es viele Softwarearchitekturen gibt, eine App zu modularisieren. Dennoch gilt die folgende allgemeine Empfehlungen können Ihnen helfen, Ihren Code lesbarer, besser wartbar überprüfbar.
Konfiguration konsistent halten
Jedes Modul führt zu einem zusätzlichen Konfigurationsaufwand. Wenn die Anzahl Ihrer Module einen bestimmten Schwellenwert erreicht, wird die Verwaltung einer konsistenten Konfiguration Herausforderung. Zum Beispiel ist es wichtig, dass Module Abhängigkeiten Version. Wenn Sie eine große Anzahl von Modulen aktualisieren müssen, Abhängigkeitsversion ist nicht nur ein Aufwand, sondern auch ein Raum für potenzielle Fehler. Um dieses Problem zu lösen, können Sie eines der Gradle-Tools verwenden, Konfiguration zentralisieren:
- Versionskataloge sind eine typsichere Liste von Abhängigkeiten. die bei der Synchronisierung von Gradle generiert werden. Hier können Sie alle Ihre Abhängigkeiten und ist für alle Module eines Projekts verfügbar.
- Verwenden Sie Konventions-Plug-ins, um die Build-Logik zwischen Modulen freizugeben.
Möglichst wenig preisgeben
Die öffentliche Oberfläche eines Moduls sollte minimal sein und nur die
die wichtigsten Infos. Es sollten keine Implementierungsdetails nach außen gelangen. Umfang
alles so weit wie möglich zu verwirklichen. private
oder internal
von Kotlin verwenden
Sichtbarkeitsbereich, um das Modul für die Deklarationen privat zu machen. Bei der Deklaration
Abhängigkeiten in Ihrem Modul verwenden, bevorzugen Sie implementation
gegenüber api
. Letzteres
stellt den Nutzern Ihres Moduls transitive Abhängigkeiten bereit. Mit
Implementierung kann die Build-Dauer verkürzen, da die Anzahl der Module reduziert wird.
die neu aufgebaut werden müssen.
Ich bevorzuge Kotlin und Java-Module
Android Studio unterstützt drei grundlegende Modultypen:
- App-Module sind ein Einstiegspunkt für Ihre Anwendung. Sie können Folgendes enthalten:
Quellcode, Ressourcen, Assets und
AndroidManifest.xml
. Die Ausgabe eines Das App-Modul ist ein Android App Bundle (AAB) oder ein Android-Anwendungspaket (APK). - Bibliotheksmodule haben denselben Inhalt wie die App-Module. Sie sind von anderen Android-Modulen als Abhängigkeit verwendet. Ausgabe eines Bibliotheksmoduls die ein Android-Archiv sind, sind strukturell identisch mit App-Modulen, werden zu einer Android Archive-Datei (AAR) kompiliert, die später von anderen Module als Abhängigkeit. Mit einem Bibliotheksmodul die gleiche Logik und Ressourcen in vielen Anwendungsmodulen kapseln und wiederverwenden.
- Die Kotlin- und Java-Bibliotheken enthalten keine Android-Ressourcen, -Assets oder Manifestdateien.
Da Android-Module viel Aufwand verursachen, empfiehlt es sich, das Modul Kotlin- oder Java-Code so gut wie möglich verwenden.