Die empfohlene App-Architektur für Android sieht vor, dass Sie Ihren Code in Klassen aufteilen, um von der Trennung von Belangen zu profitieren. Bei diesem Prinzip hat jede Klasse der Hierarchie eine einzelne definierte Aufgabe. Das führt zu mehr, kleineren Klassen, die miteinander verbunden werden müssen, um die Abhängigkeiten zu erfüllen.
Die Abhängigkeiten zwischen Klassen können als Diagramm dargestellt werden, in dem jede Klasse mit den Klassen verbunden ist, von denen sie abhängt. Die Darstellung aller Ihrer Klassen und ihrer Abhängigkeiten bildet den Anwendungsgraphen. Abbildung 1 zeigt eine Abstraktion des Anwendungsdiagramms. Wenn Klasse A (ViewModel) von Klasse B (Repository) abhängt, wird diese Abhängigkeit durch eine Linie dargestellt, die von A nach B zeigt.
Die Abhängigkeitsinjektion hilft dabei, diese Verbindungen herzustellen, und ermöglicht es Ihnen, Implementierungen für Tests auszutauschen. Wenn Sie beispielsweise eine ViewModel testen, die von einem Repository abhängt, können Sie verschiedene Implementierungen von Repository mit Fakes oder Mocks übergeben, um die verschiedenen Fälle zu testen.
Grundlagen der manuellen Abhängigkeitsinjektion
In diesem Abschnitt wird beschrieben, wie die manuelle Abhängigkeitsinjektion in einem realen Android-App-Szenario angewendet wird. Darin wird ein iterativer Ansatz beschrieben, wie Sie mit der Verwendung von Dependency Injection in Ihrer App beginnen können. Der Ansatz wird immer weiter verbessert, bis er einem Punkt entspricht, der dem sehr ähnlich ist, was Dagger automatisch für Sie generieren würde. Weitere Informationen zu Dagger finden Sie unter Dagger-Grundlagen.
Ein Ablauf ist eine Gruppe von Bildschirmen in Ihrer App, die einer Funktion entsprechen. Anmeldung, Registrierung und Kaufabwicklung sind Beispiele für Abläufe.
Bei der Beschreibung eines Anmeldevorgangs für eine typische Android-App hängt LoginActivity von LoginViewModel ab, was wiederum von UserRepository abhängt. UserRepository hängt dann von einem UserLocalDataSource und einem UserRemoteDataSource ab, die wiederum von einem Retrofit-Dienst abhängen.
LoginActivity ist der Einstiegspunkt für den Anmeldevorgang und der Nutzer interagiert mit der Aktivität. LoginActivity muss also die LoginViewModel mit allen ihren Abhängigkeiten erstellen.
Die Klassen Repository und DataSource des Ablaufs sehen so aus:
class UserRepository(
private val localDataSource: UserLocalDataSource,
private val remoteDataSource: UserRemoteDataSource
) { ... }
class UserLocalDataSource { ... }
class UserRemoteDataSource(
private val loginService: LoginRetrofitService
) { ... }
In Compose ist ComponentActivity der Einstiegspunkt. Die Abhängigkeitsverkabelung erfolgt einmal in onCreate und die Benutzeroberfläche wird durch Composables beschrieben, die von setContent aufgerufen werden:
class ApiService {
/* Your API implementation here */
}
class UserRepository(private val apiService: ApiService) {
/* Your implementation here */
}
class LoginActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// Satisfy the dependencies of LoginViewModel recursively,
// then pass what the UI needs into setContent.
val apiService = ApiService()
val userRepository = UserRepository(apiService)
setContent {
LoginScreen(userRepository)
}
}
}
@Composable
fun LoginScreen(userRepository: UserRepository) {
val viewModel: LoginViewModel = viewModel(
factory = LoginViewModelFactory(userRepository)
)
// ...
}
Dieser Ansatz hat jedoch einige Nachteile:
- Abhängigkeiten müssen in der richtigen Reihenfolge deklariert werden. Sie müssen
UserRepositoryinstanziieren, bevor SieLoginViewModelerstellen können. - Es ist schwierig, Objekte wiederzuverwenden. Wenn Sie
UserRepositoryfür mehrere Funktionen wiederverwenden möchten, muss es dem Singleton-Muster entsprechen. Das Singleton-Muster erschwert das Testen, da alle Tests dieselbe Singleton-Instanz verwenden.
Abhängigkeiten mit einem Container verwalten
Um das Problem der Wiederverwendung von Objekten zu beheben, können Sie eine eigene Abhängigkeitscontainer-Klasse erstellen, mit der Sie Abhängigkeiten abrufen. Alle von diesem Container bereitgestellten Instanzen können öffentlich sein. Im Beispiel benötigen Sie nur eine Instanz von UserRepository. Daher können Sie die zugehörigen Abhängigkeiten mit der Option, sie in Zukunft öffentlich zu machen, wenn sie bereitgestellt werden müssen, als privat festlegen:
// Container of objects shared across the whole app
class AppContainer {
// apiService and userRepository aren't private and will be exposed
val apiService = ApiService()
val userRepository = UserRepository(apiService)
}
Da diese Abhängigkeiten in der gesamten Anwendung verwendet werden, müssen sie an einem gemeinsamen Ort platziert werden, den alle Aktivitäten nutzen können: der Klasse Application. Erstellen Sie eine benutzerdefinierte Application-Klasse, die eine AppContainer-Instanz enthält.
// Custom Application class that needs to be specified
// in the AndroidManifest.xml file
class MyApplication : Application() {
// Instance of AppContainer that will be used by all the Activities of the app
val appContainer = AppContainer()
}
Mit Compose wird weiterhin dieselbe AppContainer in der Application-Unterklasse erstellt. Sie können entweder in der Aktivität darauf zugreifen, bevor Sie setContent aufrufen, oder in einem Composable mit LocalContext:
class LoginActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val appContainer = (application as MyApplication).appContainer
setContent {
LoginScreen(appContainer.userRepository)
}
}
}
// Alternatively, read AppContainer from inside a composable:
@Composable
fun LoginScreen() {
val context = LocalContext.current
val appContainer = (context.applicationContext as MyApplication).appContainer
val viewModel: LoginViewModel = viewModel(
factory = LoginViewModelFactory(appContainer.userRepository)
)
// ...
}
Wir empfehlen, Abhängigkeiten als zusammensetzbare Parameter zu übergeben, anstatt tief im Baum auf LocalContext zuzugreifen. So bleiben Composables testbar und ihre Eingaben explizit. Lösen Sie den Container einmal am Bildschirm-Root auf und geben Sie die erforderlichen Informationen nach unten weiter.
So haben Sie kein Singleton UserRepository. Stattdessen haben Sie ein AppContainer, das für alle Aktivitäten freigegeben ist und Objekte aus dem Diagramm enthält. Außerdem werden Instanzen dieser Objekte erstellt, die von anderen Klassen verwendet werden können.
Wenn LoginViewModel an mehreren Stellen in der Anwendung benötigt wird, ist es sinnvoll, einen zentralen Ort zu haben, an dem Instanzen von LoginViewModel erstellt werden.
Sie können die Erstellung von LoginViewModel in den Container verschieben und neue Objekte dieses Typs mit einer Factory bereitstellen. Der Code für ein LoginViewModelFactory sieht so aus:
// Definition of a Factory interface with a function to create objects of a type
interface Factory<T> {
fun create(): T
}
// Factory for LoginViewModel.
// Since LoginViewModel depends on UserRepository, in order to create instances of
// LoginViewModel, you need an instance of UserRepository that you pass as a parameter.
class LoginViewModelFactory(private val userRepository: UserRepository) : Factory<LoginViewModel> {
override fun create(): LoginViewModel {
return LoginViewModel(userRepository)
}
}
Bei Compose wird mit dem AppContainer-Update die Factory weiterhin verfügbar gemacht. Die Factory wird dann von der viewModel-Composable-Funktion verwendet, sodass der ViewModel-Bereich auf den nächsten ViewModelStoreOwner beschränkt ist (in der Regel die Hostaktivität oder, mit Navigation Compose, ein Navigationsziel):
// AppContainer exposing the factory (unchanged from the snippet above)
class AppContainer {
// ...
val userRepository = UserRepository(localDataSource, remoteDataSource)
val loginViewModelFactory = LoginViewModelFactory(userRepository)
}
// Compose entry point + screen composable
class LoginActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val appContainer = (application as MyApplication).appContainer
setContent {
LoginScreen(appContainer.loginViewModelFactory)
}
}
}
@Composable
fun LoginScreen(factory: LoginViewModelFactory) {
val viewModel: LoginViewModel = viewModel(factory = factory)
// ...
}
Dieser Ansatz ist besser als der vorherige, aber es gibt immer noch einige Herausforderungen:
Sie müssen
AppContainerselbst verwalten und Instanzen für alle Abhängigkeiten manuell erstellen.Es gibt immer noch viel Boilerplate-Code. Sie müssen Factories oder Parameter manuell erstellen, je nachdem, ob Sie ein Objekt wiederverwenden möchten oder nicht.
Abhängigkeiten in Anwendungsabläufen verwalten
AppContainer wird kompliziert, wenn Sie dem Projekt weitere Funktionen hinzufügen möchten. Wenn Ihre App größer wird und Sie verschiedene Funktionsabläufe einführen, treten noch mehr Probleme auf:
Wenn Sie verschiedene Flows haben, möchten Sie möglicherweise, dass Objekte nur im Bereich dieses Flows vorhanden sind. Wenn Sie beispielsweise
LoginUserDataerstellen (die möglicherweise aus dem Nutzernamen und dem Passwort besteht, die nur im Anmeldevorgang verwendet werden), möchten Sie keine Daten aus einem alten Anmeldevorgang eines anderen Nutzers beibehalten. Sie möchten für jeden neuen Ablauf eine neue Instanz. Dazu erstellen SieFlowContainer-Objekte innerhalb desAppContainer, wie im nächsten Codebeispiel gezeigt.Auch das Optimieren des Anwendungsdiagramms und der Flusscontainer kann schwierig sein. Je nach Ablauf müssen Sie daran denken, Instanzen zu löschen, die Sie nicht mehr benötigen.
Fügen wir dem Beispielcode ein LoginContainer hinzu. Sie möchten mehrere Instanzen von LoginContainer in der App erstellen können. Machen Sie es daher nicht zu einem Singleton, sondern zu einer Klasse mit den Abhängigkeiten, die der Anmeldevorgang von AppContainer benötigt.
class LoginContainer(val userRepository: UserRepository) {
val loginData = LoginUserData()
val loginViewModelFactory = LoginViewModelFactory(userRepository)
}
// AppContainer contains LoginContainer now
class AppContainer {
...
val userRepository = UserRepository(localDataSource, remoteDataSource)
// LoginContainer will be null when the user is NOT in the login flow
var loginContainer: LoginContainer? = null
}
In Compose ist die Lebensdauer des Flow-Containers an die Komposition gebunden und nicht an den Host Activity. Sie müssen ein gemeinsam genutztes AppContainer.loginContainer nicht ändern, da Composables ihre Abhängigkeiten als Parameter erhalten oder aus einem gehosteten ViewModel lesen. Es stehen zwei Optionen zur Verfügung:
- Verschachtelter Navigationsgraph in Compose (bevorzugt für Multi-Screen-Abläufe)
Platzieren Sie alle Bildschirme im Anmeldevorgang in einem verschachtelten Navigationsdiagramm und legen Sie den Bereich des Containers auf das
NavBackStackEntrydieses Diagramms fest. Der Container wird erstellt, wenn der Nutzer den Ablauf aufruft, und gelöscht, wenn der Backstack-Eintrag entfernt wird. Manuelle Lifecycle-Aufrufe sind nicht erforderlich. Weitere Informationen finden Sie unter Navigationsdiagramm entwerfen. rememberim Stammverzeichnis des Bildschirms (für einen Single-Screen-Ablauf oder wenn Sie Navigation Compose nicht verwenden). Erstellen Sie den Container inremember, damit er einmal pro Aufruf der Komposition erstellt und bereinigt wird, wenn die Composable verlassen wird:
@Composable
fun LoginFlow(appContainer: AppContainer) {
val loginContainer = remember(appContainer) {
LoginContainer(appContainer.userRepository)
}
val viewModel: LoginViewModel = viewModel(
factory = loginContainer.loginViewModelFactory
)
// Render the login flow using loginContainer.loginData and viewModel.
}
Fazit
Die Abhängigkeitsinjektion ist eine gute Methode zum Erstellen skalierbarer und testbarer Android-Apps. Sie können Container verwenden, um Instanzen von Klassen in verschiedenen Teilen Ihrer App freizugeben, und als zentralen Ort, um Instanzen von Klassen mithilfe von Factories zu erstellen.
Wenn Ihre Anwendung größer wird, werden Sie feststellen, dass Sie viel Boilerplate-Code (z. B. Factories) schreiben, was fehleranfällig sein kann. Sie müssen auch den Umfang und den Lebenszyklus der Container selbst verwalten und Container, die nicht mehr benötigt werden, optimieren und verwerfen, um Speicherplatz freizugeben. Wenn Sie das falsch machen, kann das zu subtilen Fehlern und Speicherlecks in Ihrer App führen.
Im Dagger-Abschnitt erfahren Sie, wie Sie diesen Prozess mit Dagger automatisieren und denselben Code generieren können, den Sie sonst manuell geschrieben hätten.