Używanie Daggera w aplikacjach wielomodułowych

Projekt z wieloma modułami Gradle jest nazywany projektem wielomodułowym. W przypadku projektu składającego się z wielu modułów, który jest wysyłany jako pojedynczy plik APK bez modułów funkcji, często istnieje moduł app, który może zależeć od większości modułów projektu oraz moduł base lub core, od których zwykle zależą pozostałe moduły. Moduł app zazwyczaj zawiera klasę Application, a moduł base zawiera wszystkie wspólne klasy udostępnione we wszystkich modułach w projekcie.

Moduł app to dobre miejsce do zadeklarowania komponentu aplikacji (np. ApplicationComponent na ilustracji poniżej), który może dostarczać obiekty potrzebne innym komponentom oraz jej single. Na przykład klasy takie jak OkHttpClient, parsery JSON, akcesory bazy danych lub obiekty SharedPreferences, które mogą być zdefiniowane w module core, będą dostarczane przez ApplicationComponent zdefiniowane w module app.

W module app mogą też znajdować się inne komponenty o krótszym czasie eksploatacji. Przykładem może być tu żądanie UserComponent z konfiguracją specyficzną dla użytkownika (np. UserSession) po zalogowaniu.

W różnych modułach projektu możesz zdefiniować co najmniej jeden podkomponent, którego logika jest dla niego specyficzna, tak jak to widać na ilustracji 1.

Rysunek 1. Przykład wykresu Daggera w projekcie wielomodułowym

Na przykład moduł login może zawierać zakres LoginComponent z niestandardową adnotacją @ModuleScope, która udostępnia obiekty wspólne dla danej cechy, takie jak LoginRepository. W jego module mogą znajdować się też inne komponenty zależne od obiektu LoginComponent o innym zakresie niestandardowym, np. @FeatureScope dla elementu LoginActivityComponent lub TermsAndConditionsComponent, który umożliwia bardziej precyzyjne określenie zakresu logiki, np. obiekty ViewModel.

W przypadku innych modułów, takich jak Registration, konfiguracja jest podobna.

Ogólna zasada w przypadku projektu składającego się z wielu modułów jest taka, że moduły tego samego poziomu nie powinny być od siebie zależne. Jeśli tak, zastanów się, czy ta wspólna logika (zależności między nimi) powinna znaleźć się w module nadrzędnym. Jeśli tak, przeprowadź refaktoryzację, aby przenieść klasy do modułu nadrzędnego. Jeśli tak nie jest, utwórz nowy moduł, który rozszerza moduł nadrzędny i oba moduły oryginalne.

Sprawdzoną metodą jest utworzenie komponentu w module w tych przypadkach:

  • Musisz wprowadzić wstrzykiwanie pól, tak jak w przypadku LoginActivityComponent.

  • Musisz określić zakres obiektów, tak jak w przypadku LoginComponent.

Jeśli nie ma zastosowania żadna z tych wielkości i musisz wyjaśnić Daggerowi sposób udostępniania obiektów z tego modułu, utwórz i udostępnij moduł Daggera za pomocą metod @Provides lub @Binds, jeśli w przypadku tych klas nie można przeprowadzić wstrzykiwania konstrukcji.

Implementacja z podkomponentami Daggera

Strona dokumentu Używanie Daggera w aplikacjach na Androida zawiera informacje o tworzeniu i używaniu podkomponentów. Nie możesz jednak użyć tego samego kodu, ponieważ moduły funkcji nie wiedzą o module app. Jeśli np. wyobrazisz sobie typowy proces logowania i kod przedstawiony na poprzedniej stronie, już się nie kompiluje:

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

        ...
    }
}

Powodem jest to, że moduł login nie wie o elementach MyApplication ani appComponent. Aby wszystko działało, musisz zdefiniować w module funkcji interfejs udostępniający obiekt FeatureComponent, który musi zaimplementować MyApplication.

Poniższy przykład pokazuje, jak zdefiniować interfejs LoginComponentProvider, który udostępnia LoginComponent w module login procesu logowania:

Kotlin

interface LoginComponentProvider {
    fun provideLoginComponent(): LoginComponent
}

Java

public interface LoginComponentProvider {
   public LoginComponent provideLoginComponent();
}

Teraz LoginActivity będzie używać tego interfejsu zamiast fragmentu kodu zdefiniowanego powyżej:

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

        ...
    }
}

Teraz MyApplication musi wdrożyć ten interfejs i wymagane metody:

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();
  }
}

W ten sposób możesz używać podkomponentów Daggera w projekcie wielomodułowym. W przypadku modułów funkcji rozwiązanie różni się ze względu na sposób, w jaki moduły są od siebie uzależnione.

Zależności komponentów z modułami funkcji

W modułach funkcji sposób, w jaki moduły zależą od siebie, jest odwrócony. Zamiast modułu app zawierającego moduły funkcji, moduły funkcji zależą od modułu app. Na Rysunku 2 przedstawiamy strukturę modułów.

Rysunek 2. Przykład wykresu Daggera w projekcie z modułami cech

W Dagger komponenty muszą wiedzieć o swoich podkomponentach. Informacje te znajdują się w module Dagger dodanym do komponentu nadrzędnego (takiego jak moduł SubcomponentsModule w artykule Używanie Daggera w aplikacjach na Androida).

Ponieważ zależność między aplikacją i modułem funkcji jest odwrotna, podkomponent nie jest widoczny w module app, ponieważ nie znajduje się w ścieżce kompilacji. Na przykład element LoginComponent zdefiniowany w module cech login nie może być podkomponentem elementu ApplicationComponent zdefiniowanego w module app.

Dagger ma mechanizm o nazwie zależności komponentów, który umożliwia rozwiązanie tego problemu. Komponent podrzędny nie jest więc podkomponentem komponentu nadrzędnego. Komponent podrzędny jest zależny od komponentu nadrzędnego. Nie ma więc relacji nadrzędny-podrzędny. Teraz komponenty zależą od innych, aby uzyskać określone zależności. Aby komponenty zależne mogły korzystać z nich, komponenty muszą ujawniać typy na wykresie.

Na przykład: moduł funkcji o nazwie login chce utworzyć LoginComponent, który zależy od parametru AppComponent dostępnego w module app Gradle.

Poniżej znajdziesz definicje klas i elementów AppComponent, które wchodzą w skład modułu app Gradle:

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

W module login Gradle, który zawiera moduł Gradle app, znajduje się LoginActivity, który wymaga wstrzykiwania instancji LoginViewModel:

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 wymaga zależności od UserRepository, która jest dostępna i ma zakres do AppComponent. Utwórzmy obiekt LoginComponent zależny od elementu AppComponent, który wstrzykuje LoginActivity:

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 określa zależność od elementu AppComponent, dodając ją do parametru zależności w adnotacji komponentu. Ponieważ tag LoginActivity będzie wstrzykiwany przez Daggera, dodaj do interfejsu metodę inject().

Przy tworzeniu obiektu LoginComponent trzeba przekazać instancję AppComponent. Aby to zrobić, użyj fabryki komponentów:

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

Teraz LoginActivity może utworzyć instancję LoginComponent i wywołać metodę inject().

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 wymaga UserRepository. Aby usługa LoginComponent mogła uzyskać do niej dostęp z poziomu AppComponent, AppComponent musi ją udostępnić w swoim interfejsie:

Kotlin

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

Java

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

Reguły zakresu z komponentami zależnymi działają tak samo jak w przypadku komponentów podrzędnych. LoginComponent używa instancji AppComponent, więc nie może używać tej samej adnotacji zakresu.

Jeśli chcesz ustawić zakres LoginViewModel na LoginComponent, zrób to tak jak wcześniej, korzystając z niestandardowej adnotacji @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;
    }
}

Sprawdzone metody

  • Obiekt ApplicationComponent powinien zawsze znajdować się w module app.

  • Utwórz komponenty Dagger w modułach, jeśli musisz wstrzykiwać w nich pola lub chcesz określić zakres obiektów dla określonego przepływu aplikacji.

  • W przypadku modułów Gradle, które mają być narzędziami lub pomocnikami i nie wymagają tworzenia wykresu (właśnie dlatego potrzebny jest komponent Dagger), utwórz i udostępnij publiczne moduły Daggera z metodami @Provides i @Binds w przypadku klas, które nie obsługują wstrzykiwania konstruktora.

  • Aby używać Daggera w aplikacji na Androida z modułami funkcji, użyj zależności komponentów, aby uzyskać dostęp do zależności określonych przez ApplicationComponent zdefiniowane w module app.