Die empfohlene App-Architektur von Android empfiehlt, den Code in Klassen zu unterteilen, um von der Aufgabentrennung zu profitieren. Bei diesem Prinzip hat jede Klasse der Hierarchie eine einzelne definierte Aufgabe. Dies führt zu mehr kleineren Klassen, die miteinander verbunden werden müssen, um die Abhängigkeiten voneinander zu erfüllen.

Die Abhängigkeiten zwischen Klassen können als Graph dargestellt werden, in dem jede Klasse mit den Klassen verbunden ist, von denen sie abhängt. Die Darstellung aller Klassen und ihrer Abhängigkeiten bildet die Anwendungsgrafik.
In Abbildung 1 sehen Sie eine Abstraktion des Anwendungsgraphen.
Wenn Klasse A (ViewModel
) von Klasse B (Repository
) abhängt, gibt es eine Linie, die von A nach B zeigt und diese Abhängigkeit darstellt.
Die Abhängigkeitsinjektion hilft dabei, diese Verbindungen herzustellen und ermöglicht es Ihnen, Implementierungen zum Testen auszutauschen. Wenn Sie beispielsweise eine ViewModel
testen, die von einem Repository abhängt, können Sie verschiedene Implementierungen von Repository
mit Fakes oder Mockups übergeben, um die verschiedenen Fälle zu testen.
Grundlagen der manuellen Abhängigkeitsinjektion
In diesem Abschnitt erfahren Sie, wie Sie die manuelle Abhängigkeitsinjektion in einem echten Android-App-Szenario anwenden. Es wird ein iterativer Ansatz erläutert, wie Sie mit der Dependency Injection in Ihrer App beginnen können. Der Ansatz wird verbessert, bis er einem Punkt entspricht, der dem ähnelt, was Dagger automatisch für Sie generieren würde. Weitere Informationen zu Dagger finden Sie unter Grundlagen von Dagger.
Ein Navigationsfluss ist eine Gruppe von Bildschirmen in Ihrer App, die einer Funktion entsprechen. Anmeldung, Registrierung und Bezahlung sind Beispiele für Aufrufabfolgen.
Bei einem Anmeldevorgang für eine typische Android-App hängt LoginActivity
von LoginViewModel
ab, das wiederum von UserRepository
abhängt.
Dann hängt UserRepository
von einem UserLocalDataSource
und einem UserRemoteDataSource
ab, die wiederum von einem Retrofit
-Dienst abhängen.

LoginActivity
ist der Einstiegspunkt in den Anmeldevorgang und der Nutzer interagiert mit der Aktivität. Daher muss LoginActivity
LoginViewModel
mit allen zugehörigen Abhängigkeiten erstellen.
Die Klassen Repository
und DataSource
des Ablaufs sehen so aus:
Kotlin
class UserRepository( private val localDataSource: UserLocalDataSource, private val remoteDataSource: UserRemoteDataSource ) { ... } class UserLocalDataSource { ... } class UserRemoteDataSource( private val loginService: LoginRetrofitService ) { ... }
Java
class UserLocalDataSource { public UserLocalDataSource() { } ... } class UserRemoteDataSource { private final Retrofit retrofit; public UserRemoteDataSource(Retrofit retrofit) { this.retrofit = retrofit; } ... } class UserRepository { private final UserLocalDataSource userLocalDataSource; private final UserRemoteDataSource userRemoteDataSource; public UserRepository(UserLocalDataSource userLocalDataSource, UserRemoteDataSource userRemoteDataSource) { this.userLocalDataSource = userLocalDataSource; this.userRemoteDataSource = userRemoteDataSource; } ... }
LoginActivity
sieht so aus:
Kotlin
class LoginActivity: Activity() { private lateinit var loginViewModel: LoginViewModel override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) // In order to satisfy the dependencies of LoginViewModel, you have to also // satisfy the dependencies of all of its dependencies recursively. // First, create retrofit which is the dependency of UserRemoteDataSource val retrofit = Retrofit.Builder() .baseUrl("https://example.com") .build() .create(LoginService::class.java) // Then, satisfy the dependencies of UserRepository val remoteDataSource = UserRemoteDataSource(retrofit) val localDataSource = UserLocalDataSource() // Now you can create an instance of UserRepository that LoginViewModel needs val userRepository = UserRepository(localDataSource, remoteDataSource) // Lastly, create an instance of LoginViewModel with userRepository loginViewModel = LoginViewModel(userRepository) } }
Java
public class MainActivity extends Activity { private LoginViewModel loginViewModel; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); // In order to satisfy the dependencies of LoginViewModel, you have to also // satisfy the dependencies of all of its dependencies recursively. // First, create retrofit which is the dependency of UserRemoteDataSource Retrofit retrofit = new Retrofit.Builder() .baseUrl("https://example.com") .build() .create(LoginService.class); // Then, satisfy the dependencies of UserRepository UserRemoteDataSource remoteDataSource = new UserRemoteDataSource(retrofit); UserLocalDataSource localDataSource = new UserLocalDataSource(); // Now you can create an instance of UserRepository that LoginViewModel needs UserRepository userRepository = new UserRepository(localDataSource, remoteDataSource); // Lastly, create an instance of LoginViewModel with userRepository loginViewModel = new LoginViewModel(userRepository); } }
Dieser Ansatz birgt jedoch einige Probleme:
Es gibt viel Boilerplate-Code. Wenn Sie in einem anderen Teil des Codes eine weitere Instanz von
LoginViewModel
erstellen möchten, würde es zu Codeduplizierungen kommen.Abhängigkeiten müssen in der richtigen Reihenfolge deklariert werden. Sie müssen
UserRepository
vorLoginViewModel
instanziieren, um sie zu erstellen.Es ist schwierig, Objekte wiederzuverwenden. Wenn Sie
UserRepository
für mehrere Funktionen wiederverwenden möchten, müssen Sie das Singleton-Muster verwenden. Das Singleton-Muster erschwert das Testen, da alle Tests dieselbe Singleton-Instanz verwenden.
Abhängigkeiten mit einem Container verwalten
Um das Problem mit der Wiederverwendung von Objekten zu lösen, 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. Da im Beispiel nur eine Instanz von UserRepository
benötigt wird, können Sie die Abhängigkeiten privat machen und sie bei Bedarf später öffentlich machen:
Kotlin
// Container of objects shared across the whole app class AppContainer { // Since you want to expose userRepository out of the container, you need to satisfy // its dependencies as you did before private val retrofit = Retrofit.Builder() .baseUrl("https://example.com") .build() .create(LoginService::class.java) private val remoteDataSource = UserRemoteDataSource(retrofit) private val localDataSource = UserLocalDataSource() // userRepository is not private; it'll be exposed val userRepository = UserRepository(localDataSource, remoteDataSource) }
Java
// Container of objects shared across the whole app public class AppContainer { // Since you want to expose userRepository out of the container, you need to satisfy // its dependencies as you did before private Retrofit retrofit = new Retrofit.Builder() .baseUrl("https://example.com") .build() .create(LoginService.class); private UserRemoteDataSource remoteDataSource = new UserRemoteDataSource(retrofit); private UserLocalDataSource localDataSource = new UserLocalDataSource(); // userRepository is not private; it'll be exposed public UserRepository userRepository = new UserRepository(localDataSource, remoteDataSource); }
Da diese Abhängigkeiten in der gesamten Anwendung verwendet werden, müssen sie an einer gemeinsamen Stelle platziert werden, die von allen Aktivitäten verwendet werden kann: der Klasse Application
. Erstellen Sie eine benutzerdefinierte Application
-Klasse, die eine AppContainer
-Instanz enthält.
Kotlin
// 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() }
Java
// Custom Application class that needs to be specified // in the AndroidManifest.xml file public class MyApplication extends Application { // Instance of AppContainer that will be used by all the Activities of the app public AppContainer appContainer = new AppContainer(); }
Jetzt können Sie die Instanz der AppContainer
aus der Anwendung abrufen und die freigegebene UserRepository
-Instanz abrufen:
Kotlin
class LoginActivity: Activity() { private lateinit var loginViewModel: LoginViewModel override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) // Gets userRepository from the instance of AppContainer in Application val appContainer = (application as MyApplication).appContainer loginViewModel = LoginViewModel(appContainer.userRepository) } }
Java
public class MainActivity extends Activity { private LoginViewModel loginViewModel; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); // Gets userRepository from the instance of AppContainer in Application AppContainer appContainer = ((MyApplication) getApplication()).appContainer; loginViewModel = new LoginViewModel(appContainer.userRepository); } }
So haben Sie keine Singleton-UserRepository
. Stattdessen gibt es eine AppContainer
, die für alle Aktivitäten freigegeben ist, Objekte aus dem Diagramm enthält und 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, an einem zentralen Ort Instanzen von LoginViewModel
zu erstellen. Sie können das Erstellen von LoginViewModel
in den Container verschieben und neuen Objekten dieses Typs eine Fabrik zur Verfügung stellen. Der Code für eine LoginViewModelFactory
sieht so aus:
Kotlin
// 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{ override fun create(): LoginViewModel { return LoginViewModel(userRepository) } }
Java
// Definition of a Factory interface with a function to create objects of a type public interface Factory<T> { T create(); } // 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 implements Factory{ private final UserRepository userRepository; public LoginViewModelFactory(UserRepository userRepository) { this.userRepository = userRepository; } @Override public LoginViewModel create() { return new LoginViewModel(userRepository); } }
Sie können die LoginViewModelFactory
in die AppContainer
einfügen und die LoginActivity
dazu veranlassen, sie zu verwenden:
Kotlin
// AppContainer can now provide instances of LoginViewModel with LoginViewModelFactory class AppContainer { ... val userRepository = UserRepository(localDataSource, remoteDataSource) val loginViewModelFactory = LoginViewModelFactory(userRepository) } class LoginActivity: Activity() { private lateinit var loginViewModel: LoginViewModel override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) // Gets LoginViewModelFactory from the application instance of AppContainer // to create a new LoginViewModel instance val appContainer = (application as MyApplication).appContainer loginViewModel = appContainer.loginViewModelFactory.create() } }
Java
// AppContainer can now provide instances of LoginViewModel with LoginViewModelFactory public class AppContainer { ... public UserRepository userRepository = new UserRepository(localDataSource, remoteDataSource); public LoginViewModelFactory loginViewModelFactory = new LoginViewModelFactory(userRepository); } public class MainActivity extends Activity { private LoginViewModel loginViewModel; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); // Gets LoginViewModelFactory from the application instance of AppContainer // to create a new LoginViewModel instance AppContainer appContainer = ((MyApplication) getApplication()).appContainer; loginViewModel = appContainer.loginViewModelFactory.create(); } }
Dieser Ansatz ist besser als der vorherige, aber es gibt noch einige Herausforderungen zu beachten:
Sie müssen
AppContainer
selbst verwalten und Instanzen für alle Abhängigkeiten manuell erstellen.Es gibt immer noch viel Boilerplate-Code. Sie müssen Fabriken 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 Workflows haben, möchten Sie möglicherweise, dass Objekte nur im Rahmen dieses Workflows vorhanden sind. Wenn Sie beispielsweise
LoginUserData
erstellen (das aus dem Nutzernamen und Passwort bestehen kann, die nur beim Anmeldevorgang verwendet werden), sollten Sie keine Daten aus einem alten Anmeldevorgang eines anderen Nutzers beibehalten. Sie benötigen eine neue Instanz für jeden neuen Ablauf. Dazu können SieFlowContainer
-Objekte innerhalb derAppContainer
erstellen, wie im nächsten Codebeispiel gezeigt.Auch die Optimierung der Anwendungsgrafik und der Ablaufcontainer kann schwierig sein. Je nach Ablauf müssen Sie nicht mehr benötigte Instanzen löschen.
Angenommen, Sie haben einen Anmeldevorgang, der aus einer Aktivität (LoginActivity
) und mehreren Fragmenten (LoginUsernameFragment
und LoginPasswordFragment
) besteht. Mit diesen Ansichten soll Folgendes erreicht werden:
Rufen Sie dieselbe
LoginUserData
-Instanz auf, die freigegeben werden soll, bis der Anmeldevorgang abgeschlossen ist.Erstellen Sie eine neue Instanz von
LoginUserData
, wenn der Ablauf neu gestartet wird.
Das ist mit einem Container für den Anmeldevorgang möglich. Dieser Container muss beim Start des Anmeldevorgangs erstellt und am Ende des Vorgangs aus dem Arbeitsspeicher entfernt werden.
Fügen wir dem Beispielcode ein LoginContainer
hinzu. Sie möchten mehrere Instanzen von LoginContainer
in der App erstellen können. Anstatt sie also als Singleton zu erstellen, sollten Sie sie als Klasse mit den Abhängigkeiten definieren, die der Anmeldevorgang von AppContainer
benötigt.
Kotlin
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 }
Java
// Container with Login-specific dependencies class LoginContainer { private final UserRepository userRepository; public LoginContainer(UserRepository userRepository) { this.userRepository = userRepository; loginViewModelFactory = new LoginViewModelFactory(userRepository); } public LoginUserData loginData = new LoginUserData(); public LoginViewModelFactory loginViewModelFactory; } // AppContainer contains LoginContainer now public class AppContainer { ... public UserRepository userRepository = new UserRepository(localDataSource, remoteDataSource); // LoginContainer will be null when the user is NOT in the login flow public LoginContainer loginContainer; }
Sobald Sie einen für einen Ablauf spezifischen Container haben, müssen Sie entscheiden, wann Sie die Containerinstanz erstellen und löschen möchten. Da Ihr Anmeldevorgang in einer Aktivität (LoginActivity
) enthalten ist, wird der Lebenszyklus dieses Containers von der Aktivität verwaltet. LoginActivity
kann die Instanz in onCreate()
erstellen und in onDestroy()
löschen.
Kotlin
class LoginActivity: Activity() { private lateinit var loginViewModel: LoginViewModel private lateinit var loginData: LoginUserData private lateinit var appContainer: AppContainer override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) appContainer = (application as MyApplication).appContainer // Login flow has started. Populate loginContainer in AppContainer appContainer.loginContainer = LoginContainer(appContainer.userRepository) loginViewModel = appContainer.loginContainer.loginViewModelFactory.create() loginData = appContainer.loginContainer.loginData } override fun onDestroy() { // Login flow is finishing // Removing the instance of loginContainer in the AppContainer appContainer.loginContainer = null super.onDestroy() } }
Java
public class LoginActivity extends Activity { private LoginViewModel loginViewModel; private LoginData loginData; private AppContainer appContainer; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); appContainer = ((MyApplication) getApplication()).appContainer; // Login flow has started. Populate loginContainer in AppContainer appContainer.loginContainer = new LoginContainer(appContainer.userRepository); loginViewModel = appContainer.loginContainer.loginViewModelFactory.create(); loginData = appContainer.loginContainer.loginData; } @Override protected void onDestroy() { // Login flow is finishing // Removing the instance of loginContainer in the AppContainer appContainer.loginContainer = null; super.onDestroy(); } }
Wie LoginActivity
können auch Anmeldefragmente von AppContainer
aus auf LoginContainer
zugreifen und die freigegebene LoginUserData
-Instanz verwenden.
Da es sich in diesem Fall um die Logik des Ansichtslebenszyklus handelt, ist die Verwendung der Lebenszyklusbeobachtung sinnvoll.
Fazit
Die Abhängigkeitsinjektion ist eine gute Methode, um skalierbare und testbare Android-Apps zu erstellen. Verwenden Sie Container, um Instanzen von Klassen in verschiedenen Teilen Ihrer App freizugeben und an einem zentralen Ort Instanzen von Klassen mithilfe von Fabriken 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. Außerdem müssen Sie den Umfang und den Lebenszyklus der Container selbst verwalten und nicht mehr benötigte Container optimieren und verwerfen, um Speicherplatz freizugeben. Andernfalls kann es zu subtilen Fehlern und Speicherlecks in Ihrer App kommen.
Im Abschnitt zu Dagger erfahren Sie, wie Sie diesen Prozess mit Dagger automatisieren und denselben Code generieren können, den Sie sonst manuell geschrieben hätten.