Für die von Android empfohlene App-Architektur wird empfohlen, Ihren Code in Klassen zu unterteilen, um von der Trennung der Probleme zu profitieren. Dabei handelt es sich um ein Prinzip, bei dem jede Klasse der Hierarchie eine einzelne definierte Verantwortlichkeit hat. Dies führt zu mehr, kleineren Klassen, die miteinander verbunden sein müssen, um die Abhängigkeiten der anderen zu erfüllen.
Die Abhängigkeiten zwischen den Klassen können als Graph dargestellt werden, in dem jede Klasse mit den Klassen verbunden ist, von denen sie abhängig ist. Die Darstellung all Ihrer Klassen und ihrer Abhängigkeiten besteht im Anwendungsdiagramm.
In Abbildung 1 sehen Sie eine Abstraktion der Anwendungsgrafik.
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.
Mit Abhängigkeitsinjektionen können diese Verbindungen hergestellt werden und Sie können Implementierungen zum Testen austauschen. Wenn Sie beispielsweise ein ViewModel
testen, das von einem Repository abhängt, können Sie verschiedene Implementierungen von Repository
entweder mit Fälschungen oder Simulationen übergeben, um die verschiedenen Fälle zu testen.
Grundlagen der manuellen Abhängigkeitsinjektion
In diesem Abschnitt wird beschrieben, wie Sie die manuelle Abhängigkeitsinjektion in einem echten Szenario einer Android-App anwenden. Dabei wird ein iterativer Ansatz zur Verwendung der Abhängigkeitsinjektion in Ihrer App erläutert. Der Ansatz wird verbessert, bis ein Punkt erreicht wird, der dem von Dagger automatisch für Sie erzeugten Punkt sehr ähnlich ist. 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 Bezahlvorgang sind Beispiele für Abläufe.
Wenn der Anmeldevorgang für eine typische Android-App abgedeckt wird, hängt LoginActivity
von LoginViewModel
ab, die wiederum von UserRepository
abhängt.
Dann hängt UserRepository
von UserLocalDataSource
und 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. Daher muss LoginActivity
den 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; } ... }
So sieht LoginActivity
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); } }
Bei diesem Ansatz gibt es Probleme:
Es gibt eine Menge Boilerplate-Code. Wenn Sie eine weitere Instanz von
LoginViewModel
an einem anderen Teil des Codes erstellen möchten, wäre Codeduplikate vorhanden.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 Features wiederverwenden möchten, müssen Sie dem Singleton-Muster folgen. Das Singleton-Muster erschwert die Tests, da alle Tests dieselbe Singleton-Instanz verwenden.
Abhängigkeiten mit einem Container verwalten
Um das Problem der Wiederverwendung von Objekten zu lösen, können Sie eine eigene Abhängigkeiten-Container-Klasse erstellen, mit der Sie Abhängigkeiten abrufen. Alle von diesem Container bereitgestellten Instanzen können öffentlich sein. Da Sie in diesem Beispiel nur eine Instanz von UserRepository
benötigen, können Sie die zugehörigen Abhängigkeiten als privat festlegen und sie bei Bedarf in Zukunft ö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 einem gemeinsamen Ort platziert werden, den alle Aktivitäten verwenden können: die 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 von 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); } }
Auf diese Weise haben Sie kein Singleton-UserRepository
. Stattdessen haben Sie eine AppContainer
, die für alle Aktivitäten verwendet wird, die Objekte aus dem Diagramm enthält und Instanzen dieser Objekte erstellt, die andere Klassen nutzen können.
Wenn LoginViewModel
an mehr Stellen in der Anwendung benötigt wird, ist es sinnvoll, einen zentralen Ort zu haben, an dem Sie Instanzen von LoginViewModel
erstellen. 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:
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); } }
Du kannst LoginViewModelFactory
in die AppContainer
aufnehmen und dafür sorgen, dass die LoginActivity
sie verwendet:
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, es gibt jedoch noch einige Herausforderungen:
Sie müssen
AppContainer
selbst verwalten und Instanzen für alle Abhängigkeiten manuell erstellen.Es gibt immer noch jede Menge Boilerplate-Code. Sie müssen Factorys 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 mehr Funktionen in das Projekt einbinden möchten. Wenn Ihre Anwendung größer wird und Sie mit der Einführung verschiedener Feature-Abläufe beginnen, treten noch mehr Probleme auf:
Wenn Sie verschiedene Abläufe haben, kann es sinnvoll sein, dass Objekte einfach im Bereich dieses Ablaufs leben. Wenn Sie beispielsweise
LoginUserData
erstellen, das aus dem Nutzernamen und dem Passwort bestehen kann, die nur für den Anmeldevorgang verwendet werden, möchten Sie keine Daten aus einem alten Anmeldevorgang eines anderen Nutzers beibehalten. Für jeden neuen Ablauf benötigen Sie eine neue Instanz. Dazu erstellen SieFlowContainer
-Objekte innerhalb desAppContainer
, wie im nächsten Codebeispiel gezeigt.Auch die Optimierung der Anwendungsgrafik und der Flusscontainer kann schwierig sein. Denken Sie daran, je nach aktuellem Ablauf nicht benötigte Instanzen zu löschen.
Angenommen, Ihr Anmeldevorgang besteht aus einer Aktivität (LoginActivity
) und mehreren Fragmenten (LoginUsernameFragment
und LoginPasswordFragment
). Diese Ansichten sollen Folgendes ermöglichen:
Rufen Sie dieselbe
LoginUserData
-Instanz auf, die bis zum Abschluss des Anmeldevorgangs freigegeben werden muss.Erstellen Sie eine neue Instanz von
LoginUserData
, wenn der Ablauf neu gestartet wird.
Dazu können Sie einen Container für den Anmeldevorgang verwenden. Dieser Container muss zu Beginn des Anmeldevorgangs erstellt und nach Ende des Ablaufs aus dem Arbeitsspeicher entfernt werden.
Fügen wir dem Beispielcode LoginContainer
hinzu. Sie möchten mehrere Instanzen von LoginContainer
in der Anwendung erstellen können. Anstatt sie zu einer Singleton-Instanz zu machen, sollten Sie sie zu einer Klasse mit den Abhängigkeiten machen, die der Anmeldevorgang aus der 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; }
Wenn Sie einen Container speziell für einen Ablauf haben, müssen Sie entscheiden, wann die Containerinstanz erstellt und gelöscht werden soll. Da Ihr Anmeldevorgang in einer Aktivität abgeschlossen ist (LoginActivity
), verwaltet die Aktivität den Lebenszyklus dieses Containers. 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 Anmeldefragmente auf LoginContainer
von AppContainer
zugreifen und die freigegebene LoginUserData
-Instanz verwenden.
Da Sie sich in diesem Fall mit der Logik des Ansichtslebenszyklus befassen, ist die Lebenszyklusbeobachtung sinnvoll.
Fazit
Die Abhängigkeitsinjektion ist eine gute Methode zum Erstellen skalierbarer und testbarer Android-Apps. Verwenden Sie Container, um Instanzen von Klassen in verschiedenen Teilen Ihrer Anwendung freizugeben und um Instanzen von Klassen mit Factorys zu erstellen.
Wenn Ihre Anwendung größer wird, stellen Sie fest, dass Sie viel Boilerplate-Code schreiben (z. B. Fabriken), was fehleranfällig sein kann. Außerdem müssen Sie Umfang und Lebenszyklus der Container selbst verwalten und nicht mehr benötigte Container optimieren und verwerfen, um Arbeitsspeicher freizugeben. Dies kann zu kleinen 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 andernfalls von Hand geschrieben hätten.