Auf der Seite Dagger-Grundlagen wurde erläutert, wie Dagger Ihnen helfen kann, die Abhängigkeitsinjektion in Ihre Anwendung zu automatisieren. Mit Dagger müssen Sie keinen mühsamen und fehleranfälligen Boilerplate-Code schreiben.
Best Practices – Zusammenfassung
- Verwenden Sie die Konstruktor-Injektion mit
@Inject
, um dem Dagger-Diagramm nach Möglichkeit Typen hinzuzufügen. Ist dies nicht der Fall,- Mithilfe von
@Binds
teilen Sie Dagger mit, welche Implementierung eine Schnittstelle haben soll. - Mit
@Provides
teilen Sie Dagger mit, wie Klassen bereitgestellt werden sollen, die Ihrem Projekt nicht gehören.
- Mithilfe von
- Sie sollten Module nur einmal in einer Komponente deklarieren.
- Benennen Sie die Bereichsanmerkungen abhängig von der Lebensdauer, in der die Annotation verwendet wird. Beispiele hierfür sind
@ApplicationScope
,@LoggedUserScope
und@ActivityScope
.
Abhängigkeiten hinzufügen
Wenn Sie Dagger in Ihrem Projekt verwenden möchten, fügen Sie Ihrer Anwendung in der Datei build.gradle
diese Abhängigkeiten hinzu. Die neueste Version von Dagger finden Sie in diesem GitHub-Projekt.
Kotlin
plugins { id 'kotlin-kapt' } dependencies { implementation 'com.google.dagger:dagger:2.x' kapt 'com.google.dagger:dagger-compiler:2.x' }
Java
dependencies { implementation 'com.google.dagger:dagger:2.x' annotationProcessor 'com.google.dagger:dagger-compiler:2.x' }
Dolch in Android
Nehmen wir als Beispiel eine Android-App mit dem Abhängigkeitsdiagramm aus Abbildung 1.
In Android erstellen Sie normalerweise eine Dagger-Grafik, die in Ihrer Anwendungsklasse enthalten ist, da eine Instanz des Graphen im Speicher bleiben soll, solange die Anwendung ausgeführt wird. Auf diese Weise wird das Diagramm dem App-Lebenszyklus zugeordnet. In einigen Fällen möchten Sie vielleicht auch, dass der Anwendungskontext in der Grafik verfügbar ist. Dazu muss sich das Diagramm auch in der Klasse Application
befinden. Ein Vorteil dieses Ansatzes besteht darin, dass die Grafik für andere Android-Framework-Klassen verfügbar ist.
Außerdem vereinfacht es Tests, da Sie in Tests eine benutzerdefinierte Application
-Klasse verwenden können.
Da die Schnittstelle, über die das Diagramm generiert wird, mit @Component
gekennzeichnet ist, können Sie sie ApplicationComponent
oder ApplicationGraph
nennen. Normalerweise behalten Sie eine Instanz dieser Komponente in Ihrer benutzerdefinierten Application
-Klasse und rufen sie jedes Mal auf, wenn Sie die Anwendungsgrafik benötigen, wie im folgenden Code-Snippet gezeigt:
Kotlin
// Definition of the Application graph @Component interface ApplicationComponent { ... } // appComponent lives in the Application class to share its lifecycle class MyApplication: Application() { // Reference to the application graph that is used across the whole app val appComponent = DaggerApplicationComponent.create() }
Java
// Definition of the Application graph @Component public interface ApplicationComponent { } // appComponent lives in the Application class to share its lifecycle public class MyApplication extends Application { // Reference to the application graph that is used across the whole app ApplicationComponent appComponent = DaggerApplicationComponent.create(); }
Da bestimmte Android-Framework-Klassen wie Aktivitäten und Fragmente vom System instanziiert werden, kann Dagger sie nicht für Sie erstellen. Insbesondere für Aktivitäten muss jeder Initialisierungscode in die onCreate()
-Methode übergeben werden.
Das bedeutet, dass Sie die Annotation @Inject
nicht wie in den vorherigen Beispielen im Konstruktor der Klasse (Konstruktor-Injektion) verwenden können. Stattdessen müssen Sie Field Injection verwenden.
Anstatt die Abhängigkeiten zu erstellen, die eine Aktivität in der Methode onCreate()
erfordert, soll Dagger diese Abhängigkeiten für Sie ausfüllen. Bei der Feldinjektion wenden Sie stattdessen die Annotation @Inject
auf die Felder an, die Sie aus dem Dagger-Diagramm abrufen möchten.
Kotlin
class LoginActivity: Activity() { // You want Dagger to provide an instance of LoginViewModel from the graph @Inject lateinit var loginViewModel: LoginViewModel }
Java
public class LoginActivity extends Activity { // You want Dagger to provide an instance of LoginViewModel from the graph @Inject LoginViewModel loginViewModel; }
Der Einfachheit halber ist LoginViewModel
keine ViewModel für Android-Architekturkomponenten. Es ist nur eine reguläre Klasse, die als ViewModel fungiert.
Weitere Informationen zum Injizieren dieser Klassen finden Sie im Code in der offiziellen Android Blueprints Dagger-Implementierung im Zweig dev-dagger.
Einer der Überlegungen bei Dagger ist, dass injizierte Felder nicht privat sein können. Sie müssen mindestens die Sichtbarkeit für Pakete haben, wie im vorherigen Code.
Aktivitäten einschleusen
Dagger muss wissen, dass LoginActivity
auf das Diagramm zugreifen muss, damit die erforderliche ViewModel
bereitgestellt werden kann. Auf der Seite Dagger-Grundlagen haben Sie die @Component
-Schnittstelle verwendet, um Objekte aus dem Diagramm abzurufen. Dazu haben Sie Funktionen mit dem Rückgabetyp des Diagramms verfügbar gemacht. In diesem Fall müssen Sie Dagger über ein Objekt informieren (in diesem Fall LoginActivity
), für das eine Abhängigkeit eingeschleust werden muss. Dazu stellen Sie eine Funktion bereit, die das Objekt, das die Injektion anfordert, als Parameter verwendet.
Kotlin
@Component interface ApplicationComponent { // This tells Dagger that LoginActivity requests injection so the graph needs to // satisfy all the dependencies of the fields that LoginActivity is requesting. fun inject(activity: LoginActivity) }
Java
@Component public interface ApplicationComponent { // This tells Dagger that LoginActivity requests injection so the graph needs to // satisfy all the dependencies of the fields that LoginActivity is injecting. void inject(LoginActivity loginActivity); }
Diese Funktion teilt Dagger mit, dass LoginActivity
auf die Grafik zugreifen möchte und eine Injektion anfordert. Dagger muss alle Abhängigkeiten erfüllen, die für LoginActivity
erforderlich sind (LoginViewModel
mit eigenen Abhängigkeiten).
Wenn Sie mehrere Klassen haben, die Injektion anfordern, müssen Sie alle in der Komponente mit ihrem genauen Typ deklarieren. Wenn beispielsweise LoginActivity
und RegistrationActivity
die Injektion angefordert haben, hätten Sie zwei inject()
-Methoden anstelle einer allgemeinen, die beide Fälle abdeckt. Eine generische inject()
-Methode teilt Dagger nicht mit, was bereitgestellt werden muss. Die Funktionen in der Schnittstelle können einen beliebigen Namen haben. Es ist jedoch eine Konvention in Dagger, sie beim Empfang des als Parameter zu injizierenden Objekts inject()
zu nennen.
Um ein Objekt in die Aktivität einzufügen, verwenden Sie den in der Klasse Application
definierten appComponent
, rufen die Methode inject()
auf und übergeben eine Instanz der Aktivität, die die Injektion anfordert.
Wenn Sie Aktivitäten verwenden, injizieren Sie Dagger in die Methode onCreate()
der Aktivität, bevor Sie super.onCreate()
aufrufen, um Probleme bei der Wiederherstellung von Fragmenten zu vermeiden. Während der Wiederherstellungsphase in super.onCreate()
hängt eine Aktivität Fragmente an, die möglicherweise auf Aktivitätsbindungen zugreifen möchten.
Wenn Sie Fragmente verwenden, wird Dagger in die Methode onAttach()
des Fragments eingefügt. In diesem Fall kann dies vor oder nach dem Aufruf von super.onAttach()
erfolgen.
Kotlin
class LoginActivity: Activity() { // You want Dagger to provide an instance of LoginViewModel from the graph @Inject lateinit var loginViewModel: LoginViewModel override fun onCreate(savedInstanceState: Bundle?) { // Make Dagger instantiate @Inject fields in LoginActivity (applicationContext as MyApplication).appComponent.inject(this) // Now loginViewModel is available super.onCreate(savedInstanceState) } } // @Inject tells Dagger how to create instances of LoginViewModel class LoginViewModel @Inject constructor( private val userRepository: UserRepository ) { ... }
Java
public class LoginActivity extends Activity { // You want Dagger to provide an instance of LoginViewModel from the graph @Inject LoginViewModel loginViewModel; @Override protected void onCreate(Bundle savedInstanceState) { // Make Dagger instantiate @Inject fields in LoginActivity ((MyApplication) getApplicationContext()).appComponent.inject(this); // Now loginViewModel is available super.onCreate(savedInstanceState); } } public class LoginViewModel { private final UserRepository userRepository; // @Inject tells Dagger how to create instances of LoginViewModel @Inject public LoginViewModel(UserRepository userRepository) { this.userRepository = userRepository; } }
Lassen Sie Dagger anweisen, wie die restlichen Abhängigkeiten bereitgestellt werden sollen, um die Grafik zu erstellen:
Kotlin
class UserRepository @Inject constructor( private val localDataSource: UserLocalDataSource, private val remoteDataSource: UserRemoteDataSource ) { ... } class UserLocalDataSource @Inject constructor() { ... } class UserRemoteDataSource @Inject constructor( private val loginService: LoginRetrofitService ) { ... }
Java
public class UserRepository { private final UserLocalDataSource userLocalDataSource; private final UserRemoteDataSource userRemoteDataSource; @Inject public UserRepository(UserLocalDataSource userLocalDataSource, UserRemoteDataSource userRemoteDataSource) { this.userLocalDataSource = userLocalDataSource; this.userRemoteDataSource = userRemoteDataSource; } } public class UserLocalDataSource { @Inject public UserLocalDataSource() {} } public class UserRemoteDataSource { private final LoginRetrofitService loginRetrofitService; @Inject public UserRemoteDataSource(LoginRetrofitService loginRetrofitService) { this.loginRetrofitService = loginRetrofitService; } }
Dolchmodule
In diesem Beispiel verwenden Sie die Retrofit-Netzwerkbibliothek.
UserRemoteDataSource
ist von LoginRetrofitService
abhängig. Die Methode zum Erstellen einer Instanz von LoginRetrofitService
unterscheidet sich jedoch von der bisher durchgeführten. Es handelt sich nicht um eine Klasseninstanziierung. Dies ist das Ergebnis des Aufrufs von Retrofit.Builder()
und der Übergabe verschiedener Parameter zum Konfigurieren des Anmeldedienstes.
Neben der Annotation @Inject
gibt es noch eine weitere Möglichkeit, Dagger mitzuteilen, wie eine Instanz einer Klasse bereitgestellt werden soll: die Informationen in den Dagger-Modulen. Ein Dagger-Modul ist eine Klasse, die mit @Module
annotiert ist. Dort können Sie Abhängigkeiten mit der Annotation @Provides
definieren.
Kotlin
// @Module informs Dagger that this class is a Dagger Module @Module class NetworkModule { // @Provides tell Dagger how to create instances of the type that this function // returns (i.e. LoginRetrofitService). // Function parameters are the dependencies of this type. @Provides fun provideLoginRetrofitService(): LoginRetrofitService { // Whenever Dagger needs to provide an instance of type LoginRetrofitService, // this code (the one inside the @Provides method) is run. return Retrofit.Builder() .baseUrl("https://example.com") .build() .create(LoginService::class.java) } }
Java
// @Module informs Dagger that this class is a Dagger Module @Module public class NetworkModule { // @Provides tell Dagger how to create instances of the type that this function // returns (i.e. LoginRetrofitService). // Function parameters are the dependencies of this type. @Provides public LoginRetrofitService provideLoginRetrofitService() { // Whenever Dagger needs to provide an instance of type LoginRetrofitService, // this code (the one inside the @Provides method) is run. return new Retrofit.Builder() .baseUrl("https://example.com") .build() .create(LoginService.class); } }
Module sind eine Möglichkeit, Informationen zum Bereitstellen von Objekten semantisch zu kapseln. Wie Sie sehen, haben Sie die Klasse NetworkModule
genannt, um die Logik der Bereitstellung von Objekten im Zusammenhang mit dem Netzwerk zu gruppieren. Wenn die Anwendung erweitert wird, können Sie hier auch angeben, wie ein OkHttpClient
bereitgestellt oder Gson oder Moshi konfiguriert wird.
Die Abhängigkeiten einer @Provides
-Methode sind die Parameter dieser Methode. Für die vorherige Methode kann LoginRetrofitService
ohne Abhängigkeiten angegeben werden, da die Methode keine Parameter hat. Wenn Sie einen OkHttpClient
als Parameter deklariert hätten, muss Dagger eine OkHttpClient
-Instanz aus dem Diagramm bereitstellen, um die Abhängigkeiten von LoginRetrofitService
zu erfüllen. Beispiele:
Kotlin
@Module class NetworkModule { // Hypothetical dependency on LoginRetrofitService @Provides fun provideLoginRetrofitService( okHttpClient: OkHttpClient ): LoginRetrofitService { ... } }
Java
@Module public class NetworkModule { @Provides public LoginRetrofitService provideLoginRetrofitService(OkHttpClient okHttpClient) { ... } }
Damit das Dagger-Diagramm dieses Modul kennt, müssen Sie es der @Component
-Schnittstelle so hinzufügen:
Kotlin
// The "modules" attribute in the @Component annotation tells Dagger what Modules // to include when building the graph @Component(modules = [NetworkModule::class]) interface ApplicationComponent { ... }
Java
// The "modules" attribute in the @Component annotation tells Dagger what Modules // to include when building the graph @Component(modules = NetworkModule.class) public interface ApplicationComponent { ... }
Die empfohlene Methode zum Hinzufügen von Typen zum Dagger-Diagramm ist die Konstruktor-Injektion (d.h. mit der Annotation @Inject
für den Konstruktor der Klasse).
Manchmal ist dies nicht möglich und Sie müssen Dagger-Module verwenden. Ein Beispiel hierfür ist, wenn Dagger das Ergebnis einer Berechnung verwenden soll, um zu bestimmen, wie eine Instanz eines Objekts erstellt wird. Wenn eine Instanz dieses Typs bereitgestellt werden muss, führt Dagger den Code innerhalb der Methode @Provides
aus.
So sieht das Dagger-Diagramm im Beispiel im Moment so aus:
Der Einstiegspunkt für das Diagramm ist LoginActivity
. Da LoginActivity
LoginViewModel
einfügt, erstellt Dagger eine Grafik, die eine Instanz von LoginViewModel
und rekursiv ihrer Abhängigkeiten bereitstellen kann. Dagger weiß, wie dies geht, da die Annotation @Inject
im Konstruktor der Klasse vorhanden ist.
In der von Dagger generierten ApplicationComponent
gibt es eine Factory-Methode, um Instanzen aller Klassen abzurufen, die bereitgestellt werden können. In diesem Beispiel delegiert Dagger an das in ApplicationComponent
enthaltene NetworkModule
, um eine Instanz von LoginRetrofitService
abzurufen.
Dolche-Umfänge
Bereiche wurden auf der Seite Dagger-Grundlagen erwähnt, um eine eindeutige Instanz eines Typs in einer Komponente zu erstellen. Dies wird damit bezeichnet, einen Typ dem Lebenszyklus der Komponente zuzuordnen.
Da Sie UserRepository
möglicherweise auch in anderen Funktionen der Anwendung verwenden und nicht jedes Mal ein neues Objekt erstellen möchten, können Sie es als eindeutige Instanz für die gesamte Anwendung festlegen. Das gilt auch für LoginRetrofitService
: Die Erstellung kann teuer sein und eine eindeutige Instanz dieses Objekts wieder verwenden. Eine Instanz von UserRemoteDataSource
zu erstellen ist nicht so teuer, daher ist es nicht erforderlich, sie auf den Lebenszyklus der Komponente festzulegen.
@Singleton
ist die einzige Bereichsanmerkung im Paket javax.inject
. Sie können damit ApplicationComponent
und die Objekte, die Sie in der gesamten Anwendung wiederverwenden möchten, mit Anmerkungen versehen.
Kotlin
@Singleton @Component(modules = [NetworkModule::class]) interface ApplicationComponent { fun inject(activity: LoginActivity) } @Singleton class UserRepository @Inject constructor( private val localDataSource: UserLocalDataSource, private val remoteDataSource: UserRemoteDataSource ) { ... } @Module class NetworkModule { // Way to scope types inside a Dagger Module @Singleton @Provides fun provideLoginRetrofitService(): LoginRetrofitService { ... } }
Java
@Singleton @Component(modules = NetworkModule.class) public interface ApplicationComponent { void inject(LoginActivity loginActivity); } @Singleton public class UserRepository { private final UserLocalDataSource userLocalDataSource; private final UserRemoteDataSource userRemoteDataSource; @Inject public UserRepository(UserLocalDataSource userLocalDataSource, UserRemoteDataSource userRemoteDataSource) { this.userLocalDataSource = userLocalDataSource; this.userRemoteDataSource = userRemoteDataSource; } } @Module public class NetworkModule { @Singleton @Provides public LoginRetrofitService provideLoginRetrofitService() { ... } }
Achten Sie darauf, dass keine Speicherlecks entstehen, wenn Sie Bereiche auf Objekte anwenden. Solange sich die bereichsspezifische Komponente im Arbeitsspeicher befindet, befindet sich auch das erstellte Objekt im Arbeitsspeicher. Da ApplicationComponent
beim Start der Anwendung (in der Klasse Application
) erstellt wird, wird sie beim Löschen der Anwendung ebenfalls gelöscht. Daher bleibt die eindeutige Instanz von UserRepository
immer im Arbeitsspeicher, bis die Anwendung gelöscht wird.
Dolche-Unterkomponenten
Wenn Ihr Anmeldevorgang (verwaltet von einem einzelnen LoginActivity
) aus mehreren Fragmenten besteht, sollten Sie dieselbe Instanz von LoginViewModel
in allen Fragmenten wiederverwenden. @Singleton
kann LoginViewModel
aus folgenden Gründen nicht zur Wiederverwendung der Instanz annotieren:
Die Instanz von
LoginViewModel
würde nach Abschluss des Datenflusses im Arbeitsspeicher bestehen bleiben.Sie möchten für jeden Anmeldevorgang eine andere Instanz von
LoginViewModel
. Wenn sich der Nutzer beispielsweise abmeldet, möchten Sie eine andere Instanz vonLoginViewModel
und nicht dieselbe Instanz wie bei der ersten Anmeldung des Nutzers.
Wenn Sie LoginViewModel
auf den Lebenszyklus von LoginActivity
festlegen möchten, müssen Sie eine neue Komponente (eine neue Teilgrafik) für den Anmeldevorgang und einen neuen Bereich erstellen.
Erstellen wir nun ein Diagramm speziell für den Anmeldevorgang.
Kotlin
@Component interface LoginComponent {}
Java
@Component public interface LoginComponent { }
Jetzt sollte LoginActivity
Einschleusungen von LoginComponent
erhalten, da es eine anmeldungsspezifische Konfiguration hat. Dadurch entfällt die Verantwortung für das Injizieren von LoginActivity
aus der Klasse ApplicationComponent
.
Kotlin
@Component interface LoginComponent { fun inject(activity: LoginActivity) }
Java
@Component public interface LoginComponent { void inject(LoginActivity loginActivity); }
LoginComponent
muss in der Lage sein, auf die Objekte in ApplicationComponent
zuzugreifen, da LoginViewModel
von UserRepository
abhängt. Mit Dagger-Unterkomponenten können Sie Dagger mitteilen, dass eine neue Komponente einen Teil einer anderen Komponente verwenden soll. Die neue Komponente muss eine Unterkomponente der Komponente mit gemeinsam genutzten Ressourcen sein.
Unterkomponenten sind Komponenten, die das Objektdiagramm einer übergeordneten Komponente übernehmen und erweitern. Daher sind alle Objekte, die in der übergeordneten Komponente bereitgestellt werden, auch in der Unterkomponente verfügbar. Auf diese Weise kann ein Objekt aus einer Unterkomponente von einem Objekt abhängen, das von der übergeordneten Komponente bereitgestellt wird.
Zum Erstellen von Instanzen von Unterkomponenten benötigen Sie eine Instanz der übergeordneten Komponente. Daher gelten die Objekte, die von der übergeordneten Komponente für die Unterkomponente bereitgestellt werden, weiterhin der übergeordneten Komponente.
Im Beispiel müssen Sie LoginComponent
als Unterkomponente von ApplicationComponent
definieren. Ergänzen Sie dazu LoginComponent
mit @Subcomponent
:
Kotlin
// @Subcomponent annotation informs Dagger this interface is a Dagger Subcomponent @Subcomponent interface LoginComponent { // This tells Dagger that LoginActivity requests injection from LoginComponent // so that this subcomponent graph needs to satisfy all the dependencies of the // fields that LoginActivity is injecting fun inject(loginActivity: LoginActivity) }
Java
// @Subcomponent annotation informs Dagger this interface is a Dagger Subcomponent @Subcomponent public interface LoginComponent { // This tells Dagger that LoginActivity requests injection from LoginComponent // so that this subcomponent graph needs to satisfy all the dependencies of the // fields that LoginActivity is injecting void inject(LoginActivity loginActivity); }
Außerdem müssen Sie in LoginComponent
eine Unterkomponenten-Factory definieren, damit ApplicationComponent
weiß, wie Instanzen von LoginComponent
erstellt werden können.
Kotlin
@Subcomponent interface LoginComponent { // Factory that is used to create instances of this subcomponent @Subcomponent.Factory interface Factory { fun create(): LoginComponent } fun inject(loginActivity: LoginActivity) }
Java
@Subcomponent public interface LoginComponent { // Factory that is used to create instances of this subcomponent @Subcomponent.Factory interface Factory { LoginComponent create(); } void inject(LoginActivity loginActivity); }
Um Dagger mitzuteilen, dass LoginComponent
eine Unterkomponente von ApplicationComponent
ist, müssen Sie dies so angeben:
Beim Erstellen eines neuen Dagger-Moduls (z.B.
SubcomponentsModule
) wird die Klasse der Unterkomponente an das Attributsubcomponents
der Annotation übergeben.Kotlin
// The "subcomponents" attribute in the @Module annotation tells Dagger what // Subcomponents are children of the Component this module is included in. @Module(subcomponents = LoginComponent::class) class SubcomponentsModule {}
Java
// The "subcomponents" attribute in the @Module annotation tells Dagger what // Subcomponents are children of the Component this module is included in. @Module(subcomponents = LoginComponent.class) public class SubcomponentsModule { }
So wird das neue Modul (z.B.
SubcomponentsModule
) zuApplicationComponent
hinzugefügt:Kotlin
// Including SubcomponentsModule, tell ApplicationComponent that // LoginComponent is its subcomponent. @Singleton @Component(modules = [NetworkModule::class, SubcomponentsModule::class]) interface ApplicationComponent { }
Java
// Including SubcomponentsModule, tell ApplicationComponent that // LoginComponent is its subcomponent. @Singleton @Component(modules = {NetworkModule.class, SubcomponentsModule.class}) public interface ApplicationComponent { }
Beachten Sie, dass
ApplicationComponent
dieLoginActivity
nicht mehr einschleusen muss, da diese Zuständigkeit nunLoginComponent
gehört. Daher können Sie die Methodeinject()
ausApplicationComponent
entfernen.Nutzer von
ApplicationComponent
müssen wissen, wie Instanzen vonLoginComponent
erstellt werden. Die übergeordnete Komponente muss ihrer Schnittstelle eine Methode hinzufügen, damit Nutzer Instanzen der Unterkomponente aus einer Instanz der übergeordneten Komponente erstellen können:Geben Sie die Factory, die Instanzen von
LoginComponent
erstellt, auf der Schnittstelle an:Kotlin
@Singleton @Component(modules = [NetworkModule::class, SubcomponentsModule::class]) interface ApplicationComponent { // This function exposes the LoginComponent Factory out of the graph so consumers // can use it to obtain new instances of LoginComponent fun loginComponent(): LoginComponent.Factory }
Java
@Singleton @Component(modules = { NetworkModule.class, SubcomponentsModule.class} ) public interface ApplicationComponent { // This function exposes the LoginComponent Factory out of the graph so consumers // can use it to obtain new instances of LoginComponent LoginComponent.Factory loginComponent(); }
Unterkomponenten Bereiche zuweisen
Wenn Sie das Projekt erstellen, können Sie Instanzen von ApplicationComponent
und LoginComponent
erstellen. ApplicationComponent
ist an den Lebenszyklus der Anwendung angehängt, da Sie dieselbe Instanz des Graphen verwenden möchten, solange sich die Anwendung im Arbeitsspeicher befindet.
Wie ist der Lebenszyklus von LoginComponent
? Einer der Gründe, warum Sie LoginComponent
benötigen, ist, dass Sie dieselbe Instanz von LoginViewModel
zwischen sich anmeldenden Fragmenten teilen mussten. Außerdem benötigen Sie verschiedene Instanzen von LoginViewModel
, wenn es einen neuen Anmeldevorgang gibt. LoginActivity
ist die richtige Lebensdauer für LoginComponent
: Für jede neue Aktivität benötigen Sie eine neue Instanz von LoginComponent
sowie Fragmente, die diese Instanz von LoginComponent
verwenden können.
Da LoginComponent
an den LoginActivity
-Lebenszyklus angehängt ist, müssen Sie einen Verweis auf die Komponente in der Aktivität genauso beibehalten wie den Verweis auf applicationComponent
in der Application
-Klasse. So können Fragmente
darauf zugreifen.
Kotlin
class LoginActivity: Activity() { // Reference to the Login graph lateinit var loginComponent: LoginComponent ... }
Java
public class LoginActivity extends Activity { // Reference to the Login graph LoginComponent loginComponent; ... }
Die Variable loginComponent
ist nicht mit @Inject
annotiert, da Sie nicht erwarten, dass sie von Dagger bereitgestellt wird.
Sie können mit ApplicationComponent
einen Verweis auf LoginComponent
abrufen und dann LoginActivity
so injizieren:
Kotlin
class LoginActivity: Activity() { // Reference to the Login graph lateinit var loginComponent: LoginComponent // Fields that need to be injected by the login graph @Inject lateinit var loginViewModel: LoginViewModel override fun onCreate(savedInstanceState: Bundle?) { // Creation of the login graph using the application graph loginComponent = (applicationContext as MyDaggerApplication) .appComponent.loginComponent().create() // Make Dagger instantiate @Inject fields in LoginActivity loginComponent.inject(this) // Now loginViewModel is available super.onCreate(savedInstanceState) } }
Java
public class LoginActivity extends Activity { // Reference to the Login graph LoginComponent loginComponent; // Fields that need to be injected by the login graph @Inject LoginViewModel loginViewModel; @Override protected void onCreate(Bundle savedInstanceState) { // Creation of the login graph using the application graph loginComponent = ((MyApplication) getApplicationContext()) .appComponent.loginComponent().create(); // Make Dagger instantiate @Inject fields in LoginActivity loginComponent.inject(this); // Now loginViewModel is available super.onCreate(savedInstanceState); } }
LoginComponent
wird in der Methode onCreate()
der Aktivität erstellt und implizit gelöscht, wenn die Aktivität gelöscht wird.
LoginComponent
muss bei jeder Anfrage immer dieselbe Instanz von LoginViewModel
bereitstellen. Dazu können Sie einen benutzerdefinierten Annotationsbereich erstellen und damit sowohl LoginComponent
als auch LoginViewModel
annotieren. Sie können die Annotation @Singleton
nicht verwenden, da sie bereits von der übergeordneten Komponente verwendet wird und das Objekt dadurch zu einem Anwendungs-Singleton (eindeutige Instanz für die gesamte Anwendung) wird. Sie müssen einen anderen
Annotationsbereich erstellen.
In diesem Fall hätten Sie den Bereich @LoginScope
nennen können, aber das ist keine Best Practice. Der Name der Bereichsanmerkung darf nicht den Zweck angeben, den sie erfüllt. Stattdessen sollten sie nach ihrer Lebensdauer benannt werden, da Anmerkungen von gleichgeordneten Komponenten wie RegistrationComponent
und SettingsComponent
wiederverwendet werden können. Deshalb sollten Sie sie @ActivityScope
statt @LoginScope
nennen.
Kotlin
// Definition of a custom scope called ActivityScope @Scope @Retention(value = AnnotationRetention.RUNTIME) annotation class ActivityScope // Classes annotated with @ActivityScope are scoped to the graph and the same // instance of that type is provided every time the type is requested. @ActivityScope @Subcomponent interface LoginComponent { ... } // A unique instance of LoginViewModel is provided in Components // annotated with @ActivityScope @ActivityScope class LoginViewModel @Inject constructor( private val userRepository: UserRepository ) { ... }
Java
// Definition of a custom scope called ActivityScope @Scope @Retention(RetentionPolicy.RUNTIME) public @interface ActivityScope {} // Classes annotated with @ActivityScope are scoped to the graph and the same // instance of that type is provided every time the type is requested. @ActivityScope @Subcomponent public interface LoginComponent { ... } // A unique instance of LoginViewModel is provided in Components // annotated with @ActivityScope @ActivityScope public class LoginViewModel { private final UserRepository userRepository; @Inject public LoginViewModel(UserRepository userRepository) { this.userRepository = userRepository; } }
Wenn Sie jetzt zwei Fragmente haben, die LoginViewModel
benötigen, werden beide mit derselben Instanz bereitgestellt. Wenn Sie beispielsweise eine LoginUsernameFragment
und eine LoginPasswordFragment
haben, müssen diese vom LoginComponent
eingeschleust werden:
Kotlin
@ActivityScope @Subcomponent interface LoginComponent { @Subcomponent.Factory interface Factory { fun create(): LoginComponent } // All LoginActivity, LoginUsernameFragment and LoginPasswordFragment // request injection from LoginComponent. The graph needs to satisfy // all the dependencies of the fields those classes are injecting fun inject(loginActivity: LoginActivity) fun inject(usernameFragment: LoginUsernameFragment) fun inject(passwordFragment: LoginPasswordFragment) }
Java
@ActivityScope @Subcomponent public interface LoginComponent { @Subcomponent.Factory interface Factory { LoginComponent create(); } // All LoginActivity, LoginUsernameFragment and LoginPasswordFragment // request injection from LoginComponent. The graph needs to satisfy // all the dependencies of the fields those classes are injecting void inject(LoginActivity loginActivity); void inject(LoginUsernameFragment loginUsernameFragment); void inject(LoginPasswordFragment loginPasswordFragment); }
Die Komponenten greifen auf die Instanz der Komponente zu, die im Objekt LoginActivity
enthalten ist. Der Beispielcode für LoginUserNameFragment
wird im folgenden Code-Snippet angezeigt:
Kotlin
class LoginUsernameFragment: Fragment() { // Fields that need to be injected by the login graph @Inject lateinit var loginViewModel: LoginViewModel override fun onAttach(context: Context) { super.onAttach(context) // Obtaining the login graph from LoginActivity and instantiate // the @Inject fields with objects from the graph (activity as LoginActivity).loginComponent.inject(this) // Now you can access loginViewModel here and onCreateView too // (shared instance with the Activity and the other Fragment) } }
Java
public class LoginUsernameFragment extends Fragment { // Fields that need to be injected by the login graph @Inject LoginViewModel loginViewModel; @Override public void onAttach(Context context) { super.onAttach(context); // Obtaining the login graph from LoginActivity and instantiate // the @Inject fields with objects from the graph ((LoginActivity) getActivity()).loginComponent.inject(this); // Now you can access loginViewModel here and onCreateView too // (shared instance with the Activity and the other Fragment) } }
Dasselbe gilt für LoginPasswordFragment
:
Kotlin
class LoginPasswordFragment: Fragment() { // Fields that need to be injected by the login graph @Inject lateinit var loginViewModel: LoginViewModel override fun onAttach(context: Context) { super.onAttach(context) (activity as LoginActivity).loginComponent.inject(this) // Now you can access loginViewModel here and onCreateView too // (shared instance with the Activity and the other Fragment) } }
Java
public class LoginPasswordFragment extends Fragment { // Fields that need to be injected by the login graph @Inject LoginViewModel loginViewModel; @Override public void onAttach(Context context) { super.onAttach(context); ((LoginActivity) getActivity()).loginComponent.inject(this); // Now you can access loginViewModel here and onCreateView too // (shared instance with the Activity and the other Fragment) } }
Abbildung 3 zeigt, wie das Dagger-Diagramm mit der neuen Teilkomponente aussieht. Die Klassen mit einem weißen Punkt (UserRepository
, LoginRetrofitService
und LoginViewModel
) sind diejenigen mit einer eindeutigen Instanz, die ihren jeweiligen Komponenten zugeordnet ist.
Schauen wir uns die Teile der Grafik an:
Der
NetworkModule
(und damitLoginRetrofitService
) ist inApplicationComponent
enthalten, weil Sie ihn in der Komponente angegeben haben.UserRepository
bleibt inApplicationComponent
, da er derApplicationComponent
zugeordnet ist. Wenn das Projekt wächst, möchten Sie dieselbe Instanz für verschiedene Features freigeben (z.B. Registrierung).Da
UserRepository
Teil vonApplicationComponent
ist, müssen sich auch die Abhängigkeiten (d.h.UserLocalDataSource
undUserRemoteDataSource
) in dieser Komponente befinden, damit Instanzen vonUserRepository
bereitgestellt werden können.LoginViewModel
ist inLoginComponent
enthalten, da es nur von den vonLoginComponent
eingefügten Klassen benötigt wird.LoginViewModel
ist nicht inApplicationComponent
enthalten, da keine Abhängigkeit inApplicationComponent
LoginViewModel
benötigt.Wenn Sie
UserRepository
nicht aufApplicationComponent
beschränkt hätten, hätte Dagger automatischUserRepository
und seine Abhängigkeiten als Teil vonLoginComponent
eingefügt, daUserRepository
derzeit nur an dieser Stelle verwendet wird.
Neben dem Festlegen des Umfangs von Objekten auf einen anderen Lebenszyklus ist das Erstellen von Unterkomponenten eine gute Praxis, um verschiedene Teile Ihrer Anwendung voneinander zu kapseln.
Wenn Sie Ihre Anwendung so strukturieren, dass unterschiedliche Dagger-Teilgrafiken abhängig vom Anwendungsfluss erstellt werden, können Sie eine leistungsfähigere und skalierbarere Anwendung hinsichtlich Arbeitsspeicher und Startzeit erstellen.
Best Practices beim Erstellen eines Dagger-Diagramms
Gehen Sie beim Erstellen der Dagger-Grafik für Ihre Anwendung so vor:
Beim Erstellen einer Komponente sollten Sie berücksichtigen, welches Element für die Lebensdauer dieser Komponente verantwortlich ist. In diesem Fall übernimmt die Klasse
Application
die Verantwortung fürApplicationComponent
undLoginActivity
fürLoginComponent
.Verwenden Sie den Umfang nur, wenn es sinnvoll ist. Die übermäßige Verwendung des Umfangs kann sich negativ auf die Laufzeitleistung Ihrer App auswirken: Das Objekt befindet sich im Arbeitsspeicher, solange sich die Komponente im Arbeitsspeicher befindet und das Abrufen eines Bereichs auf einem Objekt teurer ist. Wenn Dagger das Objekt bereitstellt, verwendet es die
DoubleCheck
-Sperre anstelle eines werkseitigen Anbieters.
Projekt testen, das Dagger verwendet
Einer der Vorteile von Abhängigkeitsinjektions-Frameworks wie Dagger besteht darin, dass das Testen Ihres Codes erleichtert wird.
Unit tests
Für Einheitentests müssen Sie Dagger nicht verwenden. Wenn Sie eine Klasse testen, die eine Konstruktor-Injektion verwendet, müssen Sie Dagger nicht verwenden, um diese Klasse zu instanziieren. Sie können seinen Konstruktor direkt aufrufen und fiktive oder simulierte Abhängigkeiten direkt übergeben, so wie Sie es ohne Anmerkungen tun würden.
Zum Beispiel wird LoginViewModel
so getestet:
Kotlin
@ActivityScope class LoginViewModel @Inject constructor( private val userRepository: UserRepository ) { ... } class LoginViewModelTest { @Test fun `Happy path`() { // You don't need Dagger to create an instance of LoginViewModel // You can pass a fake or mock UserRepository val viewModel = LoginViewModel(fakeUserRepository) assertEquals(...) } }
Java
@ActivityScope public class LoginViewModel { private final UserRepository userRepository; @Inject public LoginViewModel(UserRepository userRepository) { this.userRepository = userRepository; } } public class LoginViewModelTest { @Test public void happyPath() { // You don't need Dagger to create an instance of LoginViewModel // You can pass a fake or mock UserRepository LoginViewModel viewModel = new LoginViewModel(fakeUserRepository); assertEquals(...); } }
End-to-End-Tests
Für Integrationstests empfiehlt es sich, eine TestApplicationComponent
für Tests zu erstellen.
Für Produktion und Tests wird eine andere Komponentenkonfiguration verwendet.
Dies erfordert ein offeneres Design der Module in Ihrer Anwendung. Die Testkomponente erweitert die Produktionskomponente und installiert einen anderen Satz von Modulen.
Kotlin
// TestApplicationComponent extends from ApplicationComponent to have them both // with the same interface methods. You need to include the modules of the // component here as well, and you can replace the ones you want to override. // This sample uses FakeNetworkModule instead of NetworkModule @Singleton @Component(modules = [FakeNetworkModule::class, SubcomponentsModule::class]) interface TestApplicationComponent : ApplicationComponent { }
Java
// TestApplicationComponent extends from ApplicationComponent to have them both // with the same interface methods. You need to include the modules of the // Component here as well, and you can replace the ones you want to override. // This sample uses FakeNetworkModule instead of NetworkModule @Singleton @Component(modules = {FakeNetworkModule.class, SubcomponentsModule.class}) public interface TestApplicationComponent extends ApplicationComponent { }
FakeNetworkModule
enthält eine gefälschte Implementierung der ursprünglichen NetworkModule
.
Dort können Sie fiktive Instanzen oder Simulationen von beliebigen Stellen bereitstellen, die Sie ersetzen möchten.
Kotlin
// In the FakeNetworkModule, pass a fake implementation of LoginRetrofitService // that you can use in your tests. @Module class FakeNetworkModule { @Provides fun provideLoginRetrofitService(): LoginRetrofitService { return FakeLoginService() } }
Java
// In the FakeNetworkModule, pass a fake implementation of LoginRetrofitService // that you can use in your tests. @Module public class FakeNetworkModule { @Provides public LoginRetrofitService provideLoginRetrofitService() { return new FakeLoginService(); } }
In Ihrer Integration oder in End-to-End-Tests verwenden Sie einen TestApplication
, der den TestApplicationComponent
anstelle eines ApplicationComponent
erstellt.
Kotlin
// Your test application needs an instance of the test graph class MyTestApplication: MyApplication() { override val appComponent = DaggerTestApplicationComponent.create() }
Java
// Your test application needs an instance of the test graph public class MyTestApplication extends MyApplication { ApplicationComponent appComponent = DaggerTestApplicationComponent.create(); }
Anschließend wird diese Testanwendung in einem benutzerdefinierten TestRunner
verwendet, mit dem Sie Instrumentierungstests ausführen. Weitere Informationen dazu finden Sie im Codelab zur Verwendung von Dolggern im Codelab Ihrer Android-App.
Mit Dagger-Modulen arbeiten
Dagger-Module sind eine Möglichkeit, Objekte auf semantische Weise bereitzustellen. Sie können Module in Komponenten, aber auch in anderen Modulen einbinden. Dies ist eine leistungsstarke Funktion, kann aber leicht missbraucht werden.
Sobald ein Modul einer Komponente oder einem anderen Modul hinzugefügt wurde, ist es bereits in der Dagger-Grafik enthalten. Dagger kann diese Objekte in dieser Komponente bereitstellen. Prüfen Sie vor dem Hinzufügen eines Moduls, ob dieses Modul bereits Teil der Dagger-Grafik ist. Prüfen Sie dazu, ob es der Komponente bereits hinzugefügt ist, oder indem Sie das Projekt kompilieren und prüfen, ob Dagger die erforderlichen Abhängigkeiten für dieses Modul finden kann.
Als Best Practice wird empfohlen, dass Module nur einmal in einer Komponente deklariert werden sollten (außer bei bestimmten erweiterten Dagger-Anwendungsfällen).
Angenommen, Sie haben Ihre Grafik auf diese Weise konfiguriert. ApplicationComponent
enthält Module1
und Module2
, Module1
umfasst ModuleX
.
Kotlin
@Component(modules = [Module1::class, Module2::class]) interface ApplicationComponent { ... } @Module(includes = [ModuleX::class]) class Module1 { ... } @Module class Module2 { ... }
Java
@Component(modules = {Module1.class, Module2.class}) public interface ApplicationComponent { ... } @Module(includes = {ModuleX.class}) public class Module1 { ... } @Module public class Module2 { ... }
Wenn jetzt Module2
, hängt von den von ModuleX
bereitgestellten Klassen ab. Nicht empfehlenswert ist das Einfügen von ModuleX
in Module2
, da ModuleX
zweimal im Diagramm enthalten ist, wie im folgenden Code-Snippet gezeigt:
Kotlin
// Bad practice: ModuleX is declared multiple times in this Dagger graph @Component(modules = [Module1::class, Module2::class]) interface ApplicationComponent { ... } @Module(includes = [ModuleX::class]) class Module1 { ... } @Module(includes = [ModuleX::class]) class Module2 { ... }
Java
// Bad practice: ModuleX is declared multiple times in this Dagger graph. @Component(modules = {Module1.class, Module2.class}) public interface ApplicationComponent { ... } @Module(includes = ModuleX.class) public class Module1 { ... } @Module(includes = ModuleX.class) public class Module2 { ... }
Führen Sie stattdessen einen der folgenden Schritte aus:
- Refaktorieren Sie die Module und extrahieren Sie das allgemeine Modul in die Komponente.
- Erstellen Sie ein neues Modul mit den Objekten, die beide Module gemeinsam nutzen, und extrahieren Sie es in die Komponente.
Wenn Sie nicht auf diese Weise refaktorieren, sind viele Module untereinander eingeschlossen, ohne dass ein klares Organisationsgefühl vorliegt. Dadurch wird es schwieriger nachzuvollziehen, woher die einzelnen Abhängigkeiten stammen.
Gute Vorgehensweise (Option 1): ModulX wird einmal in der Dagger-Grafik deklariert.
Kotlin
@Component(modules = [Module1::class, Module2::class, ModuleX::class]) interface ApplicationComponent { ... } @Module class Module1 { ... } @Module class Module2 { ... }
Java
@Component(modules = {Module1.class, Module2.class, ModuleX.class}) public interface ApplicationComponent { ... } @Module public class Module1 { ... } @Module public class Module2 { ... }
Gute Vorgehensweise (Option 2): Gemeinsame Abhängigkeiten von Module1
und Module2
in ModuleX
werden in ein neues Modul mit dem Namen ModuleXCommon
extrahiert, das in der Komponente enthalten ist. Anschließend werden zwei weitere Module mit den Namen ModuleXWithModule1Dependencies
und ModuleXWithModule2Dependencies
mit den für jedes Modul spezifischen Abhängigkeiten erstellt. Alle Module werden einmal in der Dagger-Grafik deklariert.
Kotlin
@Component(modules = [Module1::class, Module2::class, ModuleXCommon::class]) interface ApplicationComponent { ... } @Module class ModuleXCommon { ... } @Module class ModuleXWithModule1SpecificDependencies { ... } @Module class ModuleXWithModule2SpecificDependencies { ... } @Module(includes = [ModuleXWithModule1SpecificDependencies::class]) class Module1 { ... } @Module(includes = [ModuleXWithModule2SpecificDependencies::class]) class Module2 { ... }
Java
@Component(modules = {Module1.class, Module2.class, ModuleXCommon.class}) public interface ApplicationComponent { ... } @Module public class ModuleXCommon { ... } @Module public class ModuleXWithModule1SpecificDependencies { ... } @Module public class ModuleXWithModule2SpecificDependencies { ... } @Module(includes = ModuleXWithModule1SpecificDependencies.class) public class Module1 { ... } @Module(includes = ModuleXWithModule2SpecificDependencies.class) public class Module2 { ... }
Unterstützte Injektion
Die unterstützte Injection ist ein DI-Muster, das zum Erstellen eines Objekts verwendet wird, bei dem einige Parameter vom DI-Framework bereitgestellt werden können und andere beim Erstellen vom Nutzer übergeben werden müssen.
In Android ist dieses Muster üblich in Detailbildschirmen, auf denen die ID des anzuzeigenden Elements nur während der Laufzeit bekannt ist und nicht zum Zeitpunkt der Kompilierung, wenn Dagger die DI-Grafik generiert. Weitere Informationen zur unterstützten Injektion mit Dagger finden Sie in der Dagger-Dokumentation.
Fazit
Lesen Sie den Abschnitt mit Best Practices, falls Sie das noch nicht getan haben. Informationen zur Verwendung von Dagger in einer Android-App finden Sie im Codelab zur Verwendung von Dagger in einer Android-App.