Dagger in Android-Apps verwenden

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.
  • 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.

„LoginActivity“ hängt von der Funktion „LoginViewModel“ ab, die vom UserRepository abhängig ist, das wiederum von UserLocalDataSource und UserRemoteDataSource abhängt, die wiederum von Retrofit abhängig sind.

Abbildung 1: Abhängigkeitsdiagramm des Beispielcodes

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:

Diagramm des Diagramms zur Log-in-Aktivitätsabhängigkeit

Abbildung 2: Darstellung des Diagramms mit LoginActivity, das von Dagger eingeschleust wird

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:

  1. Die Instanz von LoginViewModel würde nach Abschluss des Datenflusses im Arbeitsspeicher bestehen bleiben.

  2. Sie möchten für jeden Anmeldevorgang eine andere Instanz von LoginViewModel. Wenn sich der Nutzer beispielsweise abmeldet, möchten Sie eine andere Instanz von LoginViewModel 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:

  1. Beim Erstellen eines neuen Dagger-Moduls (z.B. SubcomponentsModule) wird die Klasse der Unterkomponente an das Attribut subcomponents 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 {
    }
    
  2. So wird das neue Modul (z.B. SubcomponentsModule) zu ApplicationComponent 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 die LoginActivity nicht mehr einschleusen muss, da diese Zuständigkeit nun LoginComponent gehört. Daher können Sie die Methode inject() aus ApplicationComponent entfernen.

    Nutzer von ApplicationComponent müssen wissen, wie Instanzen von LoginComponent 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:

  3. 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.

Anwendungsgrafik nach dem Hinzufügen der letzten Unterkomponente

Abbildung 3: Darstellung der Grafik, die Sie für die Android-App erstellt haben

Schauen wir uns die Teile der Grafik an:

  1. Der NetworkModule (und damit LoginRetrofitService) ist in ApplicationComponent enthalten, weil Sie ihn in der Komponente angegeben haben.

  2. UserRepository bleibt in ApplicationComponent, da er der ApplicationComponent zugeordnet ist. Wenn das Projekt wächst, möchten Sie dieselbe Instanz für verschiedene Features freigeben (z.B. Registrierung).

    Da UserRepository Teil von ApplicationComponent ist, müssen sich auch die Abhängigkeiten (d.h. UserLocalDataSource und UserRemoteDataSource) in dieser Komponente befinden, damit Instanzen von UserRepository bereitgestellt werden können.

  3. LoginViewModel ist in LoginComponent enthalten, da es nur von den von LoginComponent eingefügten Klassen benötigt wird. LoginViewModel ist nicht in ApplicationComponent enthalten, da keine Abhängigkeit in ApplicationComponent LoginViewModel benötigt.

    Wenn Sie UserRepository nicht auf ApplicationComponent beschränkt hätten, hätte Dagger automatisch UserRepository und seine Abhängigkeiten als Teil von LoginComponent eingefügt, da UserRepository 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ür ApplicationComponent und LoginActivity für LoginComponent.

  • 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:

  1. Refaktorieren Sie die Module und extrahieren Sie das allgemeine Modul in die Komponente.
  2. 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.