Использование Dagger в многомодульных приложениях,Использование Dagger в многомодульных приложениях

Проект с несколькими модулями Gradle известен как многомодульный проект. В многомодульном проекте, который поставляется как один APK без функциональных модулей, обычно имеется модуль app , который может зависеть от большинства модулей вашего проекта, и base или core модуль, от которого обычно зависят остальные модули. Модуль app обычно содержит класс Application , тогда как base модуль содержит все общие классы, общие для всех модулей вашего проекта.

Модуль app — хорошее место для объявления компонента вашего приложения (например, ApplicationComponent на изображении ниже), который может предоставлять объекты, которые могут понадобиться другим компонентам, а также отдельные элементы вашего приложения. Например, такие классы, как OkHttpClient , анализаторы JSON, средства доступа для вашей базы данных или объекты SharedPreferences , которые могут быть определены в core модуле, будут предоставлены ApplicationComponent , определенным в модуле app .

В модуле app также могут быть другие компоненты с более коротким сроком службы. Примером может быть UserComponent с пользовательской конфигурацией (например, UserSession ) после входа в систему.

В различных модулях вашего проекта вы можете определить хотя бы один подкомпонент с логикой, специфичной для этого модуля, как показано на рисунке 1.

Рисунок 1. Пример графа Dagger в многомодульном проекте

Например, в модуле login вы можете иметь область LoginComponent с пользовательской аннотацией @ModuleScope , которая может предоставлять объекты, общие для этой функции, такие как LoginRepository . Внутри этого модуля вы также можете иметь другие компоненты, которые зависят от LoginComponent с другой настраиваемой областью действия, например @FeatureScope для LoginActivityComponent TermsAndConditionsComponent , где вы можете использовать более специфичную для функций логику, например объекты ViewModel .

Для других модулей, таких как Registration , у вас будет аналогичная настройка.

Общее правило многомодульного проекта заключается в том, что модули одного уровня не должны зависеть друг от друга. Если да, подумайте, должна ли эта общая логика (зависимости между ними) быть частью родительского модуля. Если да, выполните рефакторинг, чтобы переместить классы в родительский модуль; если нет, создайте новый модуль, расширяющий родительский модуль, и оба исходных модуля расширят новый модуль.

Рекомендуется создавать компонент в модуле в следующих случаях:

  • Вам необходимо выполнить внедрение полей, как и в случае с LoginActivityComponent .

  • Вам необходимо определить область действия объектов, как и в случае с LoginComponent .

Если ни один из этих случаев не подходит и вам нужно указать Dagger, как предоставлять объекты из этого модуля, создайте и откройте модуль Dagger с методами @Provides или @Binds , если внедрение конструкции для этих классов невозможно.

Реализация с помощью подкомпонентов Dagger

На странице документации «Использование Dagger в приложениях Android» рассказывается, как создавать и использовать подкомпоненты. Однако вы не можете использовать один и тот же код, поскольку функциональные модули не знают о модуле app . Например, если вы думаете о типичном процессе входа в систему и коде, который есть на предыдущей странице, он больше не компилируется:

Котлин

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

Ява

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

        ...
    }
}

Причина в том, что модуль login не знает ни о MyApplication , ни appComponent . Чтобы это работало, вам необходимо определить интерфейс в функциональном модуле, который предоставляет FeatureComponent , который MyApplication должен реализовать.

В следующем примере вы можете определить интерфейс LoginComponentProvider , который предоставляет LoginComponent в модуле login для потока входа в систему:

Котлин

interface LoginComponentProvider {
    fun provideLoginComponent(): LoginComponent
}

Ява

public interface LoginComponentProvider {
   public LoginComponent provideLoginComponent();
}

Теперь LoginActivity будет использовать этот интерфейс вместо фрагмента кода, определенного выше:

Котлин

class LoginActivity: Activity() {
  ...

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

    loginComponent.inject(this)
    ...
  }
}

Ява

public class LoginActivity extends Activity {
    ...

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

        loginComponent.inject(this);

        ...
    }
}

Теперь MyApplication необходимо реализовать этот интерфейс и необходимые методы:

Котлин

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

Ява

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

Вот как вы можете использовать подкомпоненты Dagger в многомодульном проекте. С функциональными модулями решение отличается из-за того, что модули зависят друг от друга.

Зависимости компонентов с функциональными модулями

С функциональными модулями способ, которым модули обычно зависят друг от друга, меняется на противоположный. Вместо модуля app , включающего функциональные модули, функциональные модули зависят от модуля app . На рисунке 2 показано, как структурированы модули.

Рисунок 2. Пример графика Dagger в проекте с функциональными модулями

В Dagger компоненты должны знать о своих подкомпонентах. Эта информация включена в модуль Dagger, добавленный к родительскому компоненту (например, модуль SubcomponentsModule в разделе «Использование Dagger в приложениях Android »).

К сожалению, из-за обратной зависимости между приложением и функциональным модулем подкомпонент не виден из модуля app , поскольку его нет в пути сборки. Например, LoginComponent , определенный в модуле функции login не может быть подкомпонентом ApplicationComponent , определенного в модуле app .

В Dagger есть механизм, называемый зависимостями компонентов , который вы можете использовать для решения этой проблемы. Вместо того, чтобы дочерний компонент был подкомпонентом родительского компонента, дочерний компонент зависит от родительского компонента. При этом не существует отношений между родителями и детьми; теперь компоненты зависят от других, чтобы получить определенные зависимости . Компонентам необходимо предоставлять типы из графа, чтобы зависимые компоненты могли их использовать.

Например: функциональный модуль под названием login хочет создать LoginComponent , который зависит от AppComponent доступного в модуле Gradle app .

Ниже приведены определения классов и AppComponent , которые являются частью модуля Gradle app :

Котлин

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

Ява

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

В вашем модуле градиента login , который включает модуль градиента app , у вас есть LoginActivity , для которого требуется внедрение экземпляра LoginViewModel :

Котлин

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

Ява

// 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 имеет зависимость от UserRepository , которая доступна и ограничена AppComponent . Давайте создадим LoginComponent , который зависит от AppComponent для внедрения LoginActivity :

Котлин

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

Ява

// 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 указывает зависимость от AppComponent , добавляя ее в параметр зависимостей аннотации компонента. Поскольку LoginActivity будет внедрен Dagger, добавьте в интерфейс метод inject() .

При создании LoginComponent необходимо передать экземпляр AppComponent . Для этого используйте фабрику компонентов:

Котлин

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

Ява

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

Теперь LoginActivity может создать экземпляр LoginComponent и вызвать метод inject() .

Котлин

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

Ява

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 зависит от UserRepository ; и чтобы LoginComponent мог получить к нему доступ из AppComponent , AppComponent должен предоставить его в своем интерфейсе:

Котлин

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

Ява

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

Правила области действия с зависимыми компонентами работают так же, как и с подкомпонентами. Поскольку LoginComponent использует экземпляр AppComponent , они не могут использовать одну и ту же аннотацию области.

Если вы хотите ограничить LoginViewModel до LoginComponent , вы должны сделать это, как и раньше, используя пользовательскую аннотацию @ActivityScope .

Котлин

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

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

Ява

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

@ActivityScope
public class LoginViewModel {

    private final UserRepository userRepository;

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

Лучшие практики

  • ApplicationComponent всегда должен находиться в модуле app .

  • Создавайте компоненты Dagger в модулях, если вам нужно выполнить внедрение полей в этот модуль или вам нужно определить область действия объектов для определенного потока вашего приложения.

  • Для модулей Gradle, которые предназначены для использования в качестве утилит или помощников и не требуют построения графа (вот почему вам понадобится компонент Dagger), создавайте и предоставляйте общедоступные модули Dagger с помощью методов @Provides и @Binds тех классов, которые не используют Не поддерживаю внедрение конструктора.

  • Чтобы использовать Dagger в приложении Android с функциональными модулями, используйте зависимости компонентов, чтобы иметь возможность доступа к зависимостям, предоставляемым ApplicationComponent , определенным в модуле app .