Dagger in Apps mit mehreren Modulen verwenden

Ein Projekt mit mehreren Gradle-Modulen wird als Projekt mit mehreren Modulen bezeichnet. Ein Projekt mit mehreren Modulen, das als einzelnes APK ohne Featuremodule ausgeliefert wird, hat in der Regel ein app-Modul, das von den meisten Modulen Ihres Projekts abhängen kann, und ein base- oder core-Modul, von dem die restlichen Module in der Regel abhängen. Das Modul app enthält normalerweise Ihre Application-Klasse, während das Modul base alle gängigen Klassen enthält, die in allen Modulen Ihres Projekts verwendet werden.

Das Modul app eignet sich gut, um Ihre Anwendungskomponente (z. B. ApplicationComponent in der nachfolgenden Abbildung) zu deklarieren. Diese kann Objekte bereitstellen, die andere Komponenten möglicherweise benötigen, sowie die Singletons Ihrer Anwendung. Klassen wie OkHttpClient, JSON-Parser, Zugriffsfunktionen für Ihre Datenbank oder SharedPreferences-Objekte, die im core-Modul definiert werden können, werden beispielsweise über das im app-Modul definierte ApplicationComponent-Modul bereitgestellt.

Im app-Modul kannst du auch andere Komponenten mit kürzerer Lebensdauer verwenden. Ein Beispiel hierfür ist ein UserComponent mit nutzerspezifischer Konfiguration (z. B. UserSession) nach einer Anmeldung.

In den verschiedenen Modulen Ihres Projekts können Sie mindestens eine Unterkomponente mit einer für dieses Modul spezifischen Logik definieren, wie in Abbildung 1 dargestellt.

Abbildung 1: Beispiel für ein Dagger-Diagramm in einem Projekt mit mehreren Modulen

Sie können beispielsweise in einem login-Modul einen LoginComponent-Bereich mit einer benutzerdefinierten @ModuleScope-Annotation haben, die gemeinsame Objekte für dieses Feature bereitstellen kann, z. B. LoginRepository. Innerhalb dieses Moduls können Sie auch andere Komponenten haben, die von einer LoginComponent mit einem anderen benutzerdefinierten Bereich abhängig sind, z. B. @FeatureScope für einen LoginActivityComponent oder einen TermsAndConditionsComponent, wo Sie eine funktionsspezifischere Logik wie ViewModel-Objekte definieren können.

Für andere Module wie Registration gehst du ähnlich vor.

Eine allgemeine Regel bei einem Projekt mit mehreren Modulen ist, dass Module derselben Ebene nicht voneinander abhängig sein sollten. Wenn ja, prüfen Sie, ob die gemeinsame Logik (die Abhängigkeiten zwischen ihnen) Teil des übergeordneten Moduls sein sollte. Wenn ja, refaktorieren Sie die Klassen in das übergeordnete Modul. Falls nicht, erstellen Sie ein neues Modul, das das übergeordnete Modul erweitert, und lassen Sie beide ursprünglichen Module des neuen Moduls erweitern.

Als Best Practice empfiehlt es sich, in den folgenden Fällen eine Komponente in einem Modul zu erstellen:

  • Du musst, wie bei LoginActivityComponent, eine Feldeinschleusung durchführen.

  • Sie müssen den Umfang der Objekte festlegen, wie bei LoginComponent.

Wenn keine dieser Größen zutrifft und Sie Dagger mitteilen müssen, wie Objekte aus diesem Modul bereitgestellt werden sollen, erstellen Sie ein Dagger-Modul mit den Methoden @Provides oder @Binds und stellen Sie es bereit, sofern für diese Klassen keine Injektion der Konstruktion möglich ist.

Implementierung mit Dagger-Unterkomponenten

Auf der Dokumentationsseite Dolgger in Android-Apps verwenden wird beschrieben, wie Unterkomponenten erstellt und verwendet werden. Sie können jedoch nicht denselben Code verwenden, da Featuremodule das Modul app nicht kennen. Wenn Sie beispielsweise an einen typischen Anmeldevorgang und den Code auf der vorherigen Seite denken, wird keine weitere kompiliert:

Kotlin

class LoginActivity: Activity() {
  ...

  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)
    ...
  }
}

Java

public class LoginActivity extends Activity {
    ...

    @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);

        ...
    }
}

Der Grund dafür ist, dass das Modul login weder MyApplication noch appComponent kennt. Damit dies funktioniert, müssen Sie im Funktionsmodul eine Schnittstelle definieren, die eine FeatureComponent enthält, die von MyApplication implementiert werden muss.

Im folgenden Beispiel kannst du eine LoginComponentProvider-Schnittstelle definieren, die eine LoginComponent im login-Modul für den Anmeldevorgang bereitstellt:

Kotlin

interface LoginComponentProvider {
    fun provideLoginComponent(): LoginComponent
}

Java

public interface LoginComponentProvider {
   public LoginComponent provideLoginComponent();
}

Jetzt verwendet LoginActivity diese Schnittstelle anstelle des oben definierten Code-Snippets:

Kotlin

class LoginActivity: Activity() {
  ...

  override fun onCreate(savedInstanceState: Bundle?) {
    loginComponent = (applicationContext as LoginComponentProvider)
                        .provideLoginComponent()

    loginComponent.inject(this)
    ...
  }
}

Java

public class LoginActivity extends Activity {
    ...

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        loginComponent = ((LoginComponentProvider) getApplicationContext())
                                .provideLoginComponent();

        loginComponent.inject(this);

        ...
    }
}

Jetzt muss MyApplication diese Schnittstelle und die erforderlichen Methoden implementieren:

Kotlin

class MyApplication: Application(), LoginComponentProvider {
  // Reference to the application graph that is used across the whole app
  val appComponent = DaggerApplicationComponent.create()

  override fun provideLoginComponent(): LoginComponent {
    return appComponent.loginComponent().create()
  }
}

Java

public class MyApplication extends Application implements LoginComponentProvider {
  // Reference to the application graph that is used across the whole app
  ApplicationComponent appComponent = DaggerApplicationComponent.create();

  @Override
  public LoginComponent provideLoginComponent() {
    return appComponent.loginComponent.create();
  }
}

So können Sie Dagger-Unterkomponenten in einem Projekt mit mehreren Modulen verwenden. Bei Featuremodulen unterscheidet sich die Lösung durch die Art und Weise, wie die Module voneinander abhängen.

Komponentenabhängigkeiten mit Featuremodulen

Bei Featuremodulen wird die Art und Weise, wie Module normalerweise voneinander abhängig sind, umgekehrt. Anstelle des Moduls app (einschließlich Featuremodule) hängen die Funktionsmodule vom Modul app ab. Abbildung 2 zeigt, wie Module strukturiert sind.

Abbildung 2: Beispiel für ein Dagger-Diagramm in einem Projekt mit Featuremodulen

In Dagger müssen die Komponenten ihre Unterkomponenten kennen. Diese Informationen sind in einem Dagger-Modul enthalten, das der übergeordneten Komponente hinzugefügt wird (z. B. das Modul SubcomponentsModule in Dolgger in Android-Apps verwenden).

Aufgrund der umgekehrten Abhängigkeit zwischen der Anwendung und dem Funktionsmodul ist die Unterkomponente leider für das Modul app nicht sichtbar, da sie sich nicht im Build-Pfad befindet. Beispielsweise kann eine in einem login-Funktionsmodul definierte LoginComponent keine Unterkomponente des im app-Modul definierten ApplicationComponent sein.

Dagger verfügt über einen Mechanismus namens Komponentenabhängigkeiten, mit dem Sie dieses Problem lösen können. Anstatt dass die untergeordnete Komponente eine Unterkomponente der übergeordneten Komponente ist, hängt die untergeordnete Komponente von der übergeordneten Komponente ab. Es gibt also keine hierarchische Beziehung. Jetzt sind Komponenten von anderen abhängig, um bestimmte Abhängigkeiten zu erhalten. Komponenten müssen Typen aus dem Diagramm verfügbar machen, damit abhängige Komponenten sie verwenden können.

Beispiel: Ein Funktionsmodul namens login möchte eine LoginComponent erstellen, die auf dem AppComponent basiert, der im Gradle-Modul app verfügbar ist.

Im Folgenden finden Sie Definitionen für die Klassen und die AppComponent, die Teil des Gradle-Moduls app sind:

Kotlin

// UserRepository's dependencies
class UserLocalDataSource @Inject constructor() { ... }
class UserRemoteDataSource @Inject constructor() { ... }

// UserRepository is scoped to AppComponent
@Singleton
class UserRepository @Inject constructor(
    private val localDataSource: UserLocalDataSource,
    private val remoteDataSource: UserRemoteDataSource
) { ... }

@Singleton
@Component
interface AppComponent { ... }

Java

// UserRepository's dependencies
public class UserLocalDataSource {

    @Inject
    public UserLocalDataSource() {}
}

public class UserRemoteDataSource {

    @Inject
    public UserRemoteDataSource() { }
}

// UserRepository is scoped to AppComponent
@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;
    }
}

@Singleton
@Component
public interface ApplicationComponent { ... }

In Ihrem Gradle-Modul login, das das Gradle-Modul app enthält, haben Sie eine LoginActivity, für die eine LoginViewModel-Instanz eingeschleust werden muss:

Kotlin

// LoginViewModel depends on UserRepository that is scoped to AppComponent
class LoginViewModel @Inject constructor(
    private val userRepository: UserRepository
) { ... }

Java

// LoginViewModel depends on UserRepository that is scoped to AppComponent
public class LoginViewModel {

    private final UserRepository userRepository;

    @Inject
    public LoginViewModel(UserRepository userRepository) {
        this.userRepository = userRepository;
    }
}

LoginViewModel hat eine Abhängigkeit von UserRepository, die verfügbar und auf AppComponent beschränkt ist. Erstellen wir eine LoginComponent, die von AppComponent abhängig ist, um LoginActivity einzufügen:

Kotlin

// Use the dependencies attribute in the Component annotation to specify the
// dependencies of this Component
@Component(dependencies = [AppComponent::class])
interface LoginComponent {
    fun inject(activity: LoginActivity)
}

Java

// Use the dependencies attribute in the Component annotation to specify the
// dependencies of this Component
@Component(dependencies = AppComponent.class)
public interface LoginComponent {

    void inject(LoginActivity loginActivity);
}

LoginComponent gibt eine Abhängigkeit von AppComponent an, indem es dem Abhängigkeitenparameter der Komponentenanmerkung hinzugefügt wird. Da LoginActivity von Dagger eingeschleust wird, fügen Sie der Schnittstelle die Methode inject() hinzu.

Beim Erstellen eines LoginComponent muss eine Instanz von AppComponent übergeben werden. Verwenden Sie dazu die Komponenten-Factory:

Kotlin

@Component(dependencies = [AppComponent::class])
interface LoginComponent {

    @Component.Factory
    interface Factory {
        // Takes an instance of AppComponent when creating
        // an instance of LoginComponent
        fun create(appComponent: AppComponent): LoginComponent
    }

    fun inject(activity: LoginActivity)
}

Java

@Component(dependencies = AppComponent.class)
public interface LoginComponent {

    @Component.Factory
    interface Factory {
        // Takes an instance of AppComponent when creating
        // an instance of LoginComponent
        LoginComponent create(AppComponent appComponent);
    }

    void inject(LoginActivity loginActivity);
}

Jetzt kann LoginActivity eine Instanz von LoginComponent erstellen und die Methode inject() aufrufen.

Kotlin

class LoginActivity: Activity() {

    // You want Dagger to provide an instance of LoginViewModel from the Login graph
    @Inject lateinit var loginViewModel: LoginViewModel

    override fun onCreate(savedInstanceState: Bundle?) {
        // Gets appComponent from MyApplication available in the base Gradle module
        val appComponent = (applicationContext as MyApplication).appComponent

        // Creates a new instance of LoginComponent
        // Injects the component to populate the @Inject fields
        DaggerLoginComponent.factory().create(appComponent).inject(this)

        super.onCreate(savedInstanceState)

        // Now you can access loginViewModel
    }
}

Java

public class LoginActivity extends Activity {

    // You want Dagger to provide an instance of LoginViewModel from the Login graph
    @Inject
    LoginViewModel loginViewModel;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        // Gets appComponent from MyApplication available in the base Gradle module
        AppComponent appComponent = ((MyApplication) getApplicationContext()).appComponent;

        // Creates a new instance of LoginComponent
        // Injects the component to populate the @Inject fields
        DaggerLoginComponent.factory().create(appComponent).inject(this);

        // Now you can access loginViewModel
    }
}

LoginViewModel hängt von UserRepository ab. Damit LoginComponent über AppComponent darauf zugreifen kann, muss das Laufwerk von AppComponent auf seiner Schnittstelle verfügbar gemacht werden:

Kotlin

@Singleton
@Component
interface AppComponent {
    fun userRepository(): UserRepository
}

Java

@Singleton
@Component
public interface AppComponent {
    UserRepository userRepository();
}

Die Umfangsregeln mit abhängigen Komponenten funktionieren auf die gleiche Weise wie bei Unterkomponenten. Da LoginComponent eine Instanz von AppComponent nutzt, können nicht dieselbe Bereichsanmerkung verwendet werden.

Wenn Sie LoginViewModel auf LoginComponent beschränken möchten, tun Sie dies wie zuvor mit der benutzerdefinierten Annotation @ActivityScope.

Kotlin

@ActivityScope
@Component(dependencies = [AppComponent::class])
interface LoginComponent { ... }

@ActivityScope
class LoginViewModel @Inject constructor(
    private val userRepository: UserRepository
) { ... }

Java

@ActivityScope
@Component(dependencies = AppComponent.class)
public interface LoginComponent { ... }

@ActivityScope
public class LoginViewModel {

    private final UserRepository userRepository;

    @Inject
    public LoginViewModel(UserRepository userRepository) {
        this.userRepository = userRepository;
    }
}

Best Practices

  • ApplicationComponent sollte sich immer im Modul app befinden.

  • Erstellen Sie Dagger-Komponenten in Modulen, wenn Sie eine Feldeinschleusung in diesem Modul ausführen oder Objekte für einen bestimmten Ablauf Ihrer Anwendung spezifizieren müssen.

  • Für Gradle-Module, die Dienstprogramme oder Hilfsprogramme sein sollen und kein Diagramm erstellen müssen (aus diesem Grund benötigen Sie eine Dagger-Komponente), sollten Sie öffentliche Dagger-Module mit @Provides- und @Binds-Methoden der Klassen erstellen und freigeben, die keine Konstruktor-Injektion unterstützen.

  • Wenn Sie Dagger in einer Android-App mit Funktionsmodulen verwenden möchten, können Sie mithilfe von Komponentenabhängigkeiten auf Abhängigkeiten zugreifen, die von der im Modul app definierten ApplicationComponent bereitgestellt werden.