Bei der empfohlenen App-Architektur von Android ist es empfehlenswert, Code in Klassen aufteilen, um von der Trennung von Belangen zu profitieren. wobei jede Klasse der Hierarchie eine einzelne definierte Verantwortung hat. Das führt zu mehr kleineren Klassen, die miteinander verbunden werden müssen. Abhängigkeiten des jeweils anderen zu erfüllen.
Die Abhängigkeiten zwischen den Klassen können als Grafik dargestellt werden, in der jede Klasse
class ist mit den Klassen verbunden, von denen sie abhängig ist. Die Darstellung all Ihrer
Die Klassen und ihre Abhängigkeiten bilden den Anwendungsgraph.
In Abbildung 1 sehen Sie eine Abstraktion des Anwendungsdiagramms.
Wenn Klasse A (ViewModel
) von Klasse B (Repository
) abhängt, gibt es
eine Linie, die von A nach B zeigt,
die diese Abhängigkeit darstellt.
Die Abhängigkeitsinjektion hilft beim Erstellen dieser Verbindungen und ermöglicht es Ihnen,
Implementierungen für Tests. Beim Testen einer ViewModel
der von einem Repository abhängig ist, können Sie
Repository
mit Fälschungen oder Modellen, um die verschiedenen Fälle zu testen.
Grundlagen der manuellen Abhängigkeitsinjektion
In diesem Abschnitt wird beschrieben, wie die manuelle Abhängigkeitsinjektion bei einem echten Android-Gerät App-Szenarios zu erstellen. Es wird ein iterativer Ansatz erläutert, wie Sie beginnen können, mit Abhängigkeitsinjektion in Ihre App. Der Ansatz verbessert sich, bis einen Punkt, der dem von Dagger automatisch generierten, von dir. Weitere Informationen zu Dagger finden Sie unter Dagger-Grundlagen.
Ein Ablauf ist eine Gruppe von Bildschirmen in Ihrer App, die einem . Anmeldung, Registrierung und Bezahlvorgang sind Beispiele für Abläufe.
Bei der Erläuterung des Anmeldevorgangs für eine typische Android-App zeigt der LoginActivity
hängt von LoginViewModel
ab, das wiederum von UserRepository
abhängt.
Dann hängt UserRepository
von einem UserLocalDataSource
und einem
UserRemoteDataSource
, das wiederum von einem Retrofit
abhängig ist
.
LoginActivity
ist der Einstiegspunkt für den Anmeldevorgang und den Nutzer
mit der Aktivität interagiert. Daher muss LoginActivity
den Parameter
LoginViewModel
mit allen zugehörigen Abhängigkeiten.
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 erstellen möchten, von
LoginViewModel
an einem anderen Teil des Codes.Abhängigkeiten müssen der Reihe nach deklariert werden. Sie müssen eine Instanz
UserRepository
vorLoginViewModel
, um sie zu erstellen.Es ist schwierig, Objekte wiederzuverwenden. Wenn Sie
UserRepository
wiederverwenden möchten mehrere Funktionen nutzen, müsstest du dafür sorgen, Singleton-Muster. Das Singleton-Muster erschwert das Testen, da alle Tests den gleichen Singleton-Instanz verwendet.
Abhängigkeiten mit einem Container verwalten
Um das Problem der Wiederverwendung von Objekten zu lösen, können Sie Ihre eigenen Objekte erstellen
dependencies container, mit der Sie Abhängigkeiten abrufen. Alle Instanzen
von diesem Container bereitgestellt werden, können öffentlich sein. Da Sie in diesem Beispiel nur
eine Instanz von UserRepository
ist, können Sie deren Abhängigkeiten mit der
die Möglichkeit, sie in Zukunft zu veröffentlichen, falls sie zur Verfügung gestellt werden müssen:
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 zentralen Ort platziert werden, den alle Aktivitäten nutzen können: das
Klasse Application
. Benutzerdefinierten
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
Rufen Sie den gemeinsamen Wert der Instanz UserRepository
ab:
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 kein Singleton-UserRepository
-Element. Stattdessen haben Sie eine
AppContainer
wird für alle Aktivitäten verwendet, die Objekte aus dem Diagramm enthalten
und erstellt Instanzen dieser Objekte, die andere Klassen nutzen können.
Wenn LoginViewModel
an mehr Stellen in der Anwendung benötigt wird, hat ein
zentraler Ort, an dem Sie Instanzen von LoginViewModel
erstellen,
Sinn. Sie können die Erstellung von LoginViewModel
in den Container verschieben und Folgendes angeben:
dieses Typs mit einer Factory erstellen. 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); } }
Sie können die LoginViewModelFactory
in die AppContainer
aufnehmen und die
LoginActivity
verbraucht sie:
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 zu berücksichtigende Herausforderungen:
Sie müssen
AppContainer
selbst verwalten und Instanzen für alle erstellen von Hand.Es gibt immer noch viel Boilerplate-Code. Sie müssen Fabriken oder -Parameter manuell hinzufügen, 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
für das Projekt. Wenn Ihre App größer wird und Sie verschiedene
entstehen noch mehr Probleme:
Bei unterschiedlichen Abläufen kann es sinnvoll sein, Objekte nur in der den Umfang dieses Ablaufs. Wenn Sie beispielsweise
LoginUserData
(das könnte aus dem Nutzernamen und dem Passwort bestehen, die nur bei der Anmeldung verwendet werden. um Daten aus einem alten Anmeldevorgang eines anderen Nutzers beizubehalten. Sie möchten eine neue für jeden neuen Ablauf. Dazu erstellen SieFlowContainer
-Objekten innerhalb derAppContainer
, wie im nächsten Codebeispiel gezeigt.Die Optimierung des Anwendungsdiagramms und der Flusscontainer kann ebenfalls schwierig sein. Denken Sie daran, nicht benötigte Instanzen zu löschen, je nachdem, in dem Sie sich befinden.
Angenommen, Sie haben einen Anmeldevorgang, der aus einer Aktivität besteht (LoginActivity
)
und mehrere Fragmente (LoginUsernameFragment
und LoginPasswordFragment
) enthalten.
Für diese Datenansichten gilt Folgendes:
Greifen Sie auf dieselbe
LoginUserData
-Instanz zu, die bis zum Anmeldevorgang abgeschlossen.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 wird erstellt, wenn der Anmeldevorgang beginnt, und wird aus dem Arbeitsspeicher entfernt, wenn der Vorgang endet.
Fügen Sie dem Beispielcode ein LoginContainer
hinzu. Sie möchten in der Lage sein,
mehrere Instanzen von LoginContainer
in der App zu erstellen.
Singleton, machen Sie daraus eine Klasse mit den Abhängigkeiten,
AppContainer
.
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 einen Container erstellen möchten
und löschen Sie die Containerinstanz. Da der Anmeldevorgang in der
Eine Aktivität (LoginActivity
), die den Lebenszyklus verwaltet
des 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 das LoginContainer
von
AppContainer
und verwenden die freigegebene Instanz LoginUserData
.
In diesem Fall haben Sie es mit der Lebenszykluslogik der Ansicht zu tun, Beobachtungen des Lebenszyklus sinnvoll.
Fazit
Die Abhängigkeitsinjektion ist eine gute Technik, um skalierbare und testbare Android-Apps. Mit Containern können Sie Instanzen von Klassen in verschiedenen und als zentraler Ort zum Erstellen von Instanzen Ihrer App Klassen mithilfe von Factorys.
Wenn Ihre Anwendung größer wird, werden Sie feststellen, Boilerplate-Code (z. B. Fabriken), der fehleranfällig sein kann. Außerdem müssen Sie Umfang und Lebenszyklus der Container selbst verwalten, Verwerfen von Containern, die nicht mehr benötigt werden, um Arbeitsspeicher freizugeben. Dies kann zu kleinen Fehlern und Speicherlecks in Ihrer App führen.
Im Abschnitt Dagger führen Sie folgende Schritte aus: erfahren Sie, wie Sie mit Dagger diesen Prozess automatisieren und denselben Code generieren können, die Sie sonst per Hand geschrieben hätten.