Как использовать Dagger в приложениях для Android

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

Общие рекомендации

  • По возможности используйте внедрение через конструктор с @Inject, чтобы добавлять типы в граф Dagger. Если она отключена:
    • Используйте @Binds, чтобы указать Dagger, какую реализацию должен иметь интерфейс.
    • Используйте @Provides, чтобы указать Dagger, как предоставлять классы, которые не принадлежат вашему проекту.
  • Модули в компоненте нужно объявлять только один раз.
  • Назовите аннотации области видимости в зависимости от времени жизни, в котором используется аннотация. Примеры: @ApplicationScope, @LoggedUserScope и @ActivityScope.

Добавление зависимостей

Чтобы использовать Dagger в проекте, добавьте в файл build.gradle приложения следующие зависимости: Последнюю версию Dagger можно найти в этом проекте GitHub.

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

Dagger в Android

Рассмотрим пример приложения для Android с графиком зависимостей, показанным на рисунке 1.

LoginActivity зависит от LoginViewModel, который зависит от UserRepository, который зависит от UserLocalDataSource и UserRemoteDataSource, который, в свою очередь, зависит от Retrofit.

Рисунок 1. График зависимостей для примера кода

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

Поскольку интерфейс, генерирующий граф, аннотирован с помощью @Component, вы можете вызвать его ApplicationComponent или ApplicationGraph. Обычно экземпляр этого компонента хранится в пользовательском классе Application и вызывается каждый раз, когда требуется граф приложения, как показано в следующем фрагменте кода:

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

Поскольку некоторые классы фреймворка Android, такие как действия и фрагменты, создаются системой, Dagger не может создать их за вас. В частности, для действий любой код инициализации должен быть помещен в метод onCreate(). Это означает, что вы не можете использовать аннотацию @Inject в конструкторе класса (внедрение через конструктор), как в предыдущих примерах. Вместо этого вам нужно использовать внедрение через поле.

Вместо того чтобы создавать зависимости, необходимые для действия, в методе onCreate(), вы хотите, чтобы Dagger заполнил эти зависимости за вас. Для внедрения через поле вы вместо этого применяете аннотацию @Inject к полям, которые хотите получить из графа Dagger.

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

Для простоты LoginViewModel не является компонентом архитектуры Android ViewModel. Это обычный класс, который действует как ViewModel. Дополнительную информацию о том, как внедрять эти классы, можно найти в коде официальной реализации Dagger в Android Blueprints в ветке dev-dagger.

Одно из соображений, связанных с Dagger, заключается в том, что внедренные поля не могут быть частными. Они должны иметь по крайней мере видимость на уровне пакета, как в предыдущем коде.

Внедрение действий

Dagger должен знать, что LoginActivity должен получить доступ к графу, чтобы предоставить необходимый ViewModel. На странице Основы Dagger вы использовали интерфейс @Component, чтобы получать объекты из графа, предоставляя функции с типом возвращаемого значения, которое вы хотите получить из графа. В этом случае вам нужно сообщить Dagger об объекте (в данном случае LoginActivity), которому требуется внедрение зависимости. Для этого вы предоставляете функцию, которая принимает в качестве параметра объект, запрашивающий внедрение.

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

Эта функция сообщает Dagger, что LoginActivity хочет получить доступ к графу и запрашивает внедрение. Dagger должен удовлетворять всем зависимостям, которые требуются для LoginActivity (LoginViewModel с собственными зависимостями). Если у вас есть несколько классов, запрашивающих внедрение, вы должны явно объявить их все в компоненте с их точным типом. Например, если у вас есть LoginActivity и RegistrationActivity, запрашивающие внедрение, у вас будет два метода inject() вместо одного общего, охватывающего оба случая. Общий метод inject() не сообщает Dagger, что нужно предоставить. Функции в интерфейсе могут иметь любое название, но в Dagger принято называть их inject(), когда они получают объект для внедрения в качестве параметра.

Чтобы внедрить объект в действие, используйте appComponent, определенный в классе Application, и вызовите метод inject(), передав экземпляр действия, запрашивающего внедрение.

При использовании действий внедряйте Dagger в метод onCreate() действия перед вызовом super.onCreate(), чтобы избежать проблем с восстановлением фрагментов. На этапе восстановления в super.onCreate() действие прикрепляет фрагменты, которым может потребоваться доступ к привязкам действия.

При использовании фрагментов внедрите Dagger в метод onAttach() фрагмента. В этом случае его можно вызвать до или после super.onAttach().

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

Давайте расскажем Dagger, как предоставить остальные зависимости для построения графа:

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

Модули Dagger

В этом примере используется сетевая библиотека Retrofit. UserRemoteDataSource зависит от LoginRetrofitService. Однако способ создания экземпляра LoginRetrofitService отличается от того, что вы делали до сих пор. Это не экземпляр класса, а результат вызова Retrofit.Builder() и передачи различных параметров для настройки сервиса входа.

Помимо аннотации @Inject, есть ещё один способ сообщить Dagger, как предоставить экземпляр класса: информация внутри модулей Dagger. Модуль Dagger – это класс, аннотированный с помощью @Module. В нем можно определить зависимости с помощью аннотации @Provides.

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

Модули позволяют семантически инкапсулировать информацию о том, как предоставлять объекты. Как видите, вы назвали класс NetworkModule, чтобы сгруппировать логику предоставления объектов, связанных с сетью. Если приложение расширяется, вы также можете добавить сюда информацию о том, как предоставить OkHttpClient или как настроить Gson или Moshi.

Зависимости метода @Provides – это его параметры. Для предыдущего метода LoginRetrofitService можно указать без зависимостей, так как у него нет параметров. Если бы вы объявили OkHttpClient в качестве параметра, Dagger потребовалось бы предоставить экземпляр OkHttpClient из графа, чтобы удовлетворить зависимости LoginRetrofitService. Пример:

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

Чтобы граф Dagger знал об этом модуле, добавьте его в интерфейс @Component следующим образом:

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

Рекомендуемый способ добавления типов в граф Dagger – внедрение через конструктор (с помощью аннотации @Inject в конструкторе класса). Иногда это невозможно, и вам приходится использовать модули Dagger. Например, когда вы хотите, чтобы Dagger использовал результат вычисления для определения того, как создать экземпляр объекта. Всякий раз, когда ему нужно предоставить экземпляр этого типа, Dagger запускает код внутри метода @Provides.

Вот как сейчас выглядит граф Dagger в примере:

Диаграмма графа зависимостей LoginActivity

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

Точка входа в граф – LoginActivity. Поскольку LoginActivity внедряет LoginViewModel, Dagger создает граф, который знает, как предоставить экземпляр LoginViewModel и рекурсивно его зависимости. Dagger знает, как это сделать, благодаря аннотации @Inject в конструкторе классов.

Внутри ApplicationComponent, сгенерированного Dagger, есть метод фабричного типа для получения экземпляров всех классов, которые он может предоставить. В этом примере Dagger делегирует NetworkModule, включенному в ApplicationComponent, получение экземпляра LoginRetrofitService.

Области действия Dagger

Области действия упоминались на странице Основы Dagger как способ получить уникальный экземпляр типа в компоненте. Это и называется привязкой типа к жизненному циклу компонента.

Поскольку вы можете использовать UserRepository в других функциях приложения и не хотите создавать новый объект каждый раз, когда он вам нужен, вы можете назначить его уникальным экземпляром для всего приложения. То же самое касается LoginRetrofitService: его создание может быть дорогостоящим, и вы также хотите, чтобы уникальный экземпляр этого объекта использовался повторно. Создание экземпляра UserRemoteDataSource не требует больших ресурсов, поэтому ограничивать его жизненным циклом компонента не нужно.

@Singleton – единственная аннотация области видимости, которая поставляется с пакетом javax.inject. Вы можете использовать ее для аннотирования ApplicationComponent и объектов, которые хотите повторно использовать во всем приложении.

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

Будьте осторожны, чтобы не допустить утечки памяти при применении областей видимости к объектам. Пока компонент находится в памяти, созданный объект также находится в памяти. Поскольку ApplicationComponent создается при запуске приложения (в классе Application), он удаляется при удалении приложения. Таким образом, уникальный экземпляр UserRepository всегда остается в памяти до тех пор, пока приложение не будет удалено.

Дочерние компоненты Dagger

Если процесс входа (управляемый одним LoginActivity) состоит из нескольких фрагментов, во всех них следует использовать один и тот же экземпляр LoginViewModel. @Singleton не может аннотировать LoginViewModel, чтобы повторно использовать экземпляр, по следующим причинам:

  1. Экземпляр LoginViewModel останется в памяти после завершения процесса.

  2. Для каждого процесса входа нужен отдельный экземпляр LoginViewModel. Например, если пользователь выходит из системы, вам нужен другой экземпляр LoginViewModel, а не тот же, что и при первом входе пользователя в систему.

Чтобы ограничить область действия LoginViewModel жизненным циклом LoginActivity, вам нужно создать новый компонент (новый подграф) для процесса входа и новую область действия.

Давайте создадим граф, относящийся к процессу входа.

Kotlin

@Component
interface LoginComponent {}

Java

@Component
public interface LoginComponent {
}

Теперь LoginActivity должен получать инъекции от LoginComponent, поскольку у него есть конфигурация, специфичная для входа. Это снимает ответственность за внедрение LoginActivity из класса ApplicationComponent.

Kotlin

@Component
interface LoginComponent {
    fun inject(activity: LoginActivity)
}

Java

@Component
public interface LoginComponent {
    void inject(LoginActivity loginActivity);
}

LoginComponent должен иметь доступ к объектам из ApplicationComponent, потому что LoginViewModel зависит от UserRepository. Чтобы сообщить Dagger, что вы хотите, чтобы новый компонент использовал часть другого компонента, используйте субкомпоненты Dagger. Новый компонент должен быть дочерним по отношению к компоненту, содержащему общие ресурсы.

Дочерние компоненты наследуют и расширяют граф объектов родительского компонента. Таким образом, все объекты, предоставляемые в родительском компоненте, также предоставляются в подкомпоненте. Таким образом, объект из подкомпонента может зависеть от объекта, предоставляемого родительским компонентом.

Чтобы создать экземпляры дочерних компонентов, вам нужен экземпляр родительского компонента. Поэтому объекты, предоставляемые родительским компонентом дочернему, по-прежнему относятся к родительскому компоненту.

В нашем примере LoginComponent нужно определить как дочерний компонент ApplicationComponent. Для этого добавьте в LoginComponent аннотацию @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);
}

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

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

Чтобы сообщить Dagger, что LoginComponent является подкомпонентом ApplicationComponent, необходимо указать это следующим образом:

  1. Создайте новый модуль Dagger (например, SubcomponentsModule), передав класс дочернего компонента в атрибут subcomponents аннотации.

    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. Добавление нового модуля (например, SubcomponentsModule) в ApplicationComponent:

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

    Обратите внимание, что ApplicationComponent больше не нужно внедрять LoginActivity, поскольку теперь за это отвечает LoginComponent, поэтому вы можете удалить метод inject() из ApplicationComponent.

    Пользователям ApplicationComponent нужно знать, как создавать экземпляры LoginComponent. Родительский компонент должен добавить в свой интерфейс метод, позволяющий пользователям создавать экземпляры дочернего компонента из экземпляра родительского компонента:

  3. Предоставьте фабрику, которая создает экземпляры LoginComponent в интерфейсе:

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

Назначение областей действия дочерним компонентам

Если вы создадите проект, то сможете создавать экземпляры как ApplicationComponent, так и LoginComponent. ApplicationComponent привязан к жизненному циклу приложения, поскольку вы хотите использовать один и тот же экземпляр графа, пока приложение находится в памяти.

Каков жизненный цикл LoginComponent? Одна из причин, по которой вам понадобился LoginComponent, заключается в том, что вам нужно было использовать один и тот же экземпляр LoginViewModel между фрагментами, связанными со входом в систему. Но также вам нужны разные экземпляры LoginViewModel при каждом новом процессе входа. LoginActivity – это правильный срок действия для LoginComponent. Для каждого нового действия нужен новый экземпляр LoginComponent и фрагменты, которые могут использовать этот экземпляр LoginComponent.

Поскольку LoginComponent привязан к жизненному циклу LoginActivity, вам нужно сохранить ссылку на компонент в действии так же, как вы сохранили ссылку на applicationComponent в классе Application. Таким образом, фрагменты могут получить к нему доступ.

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;

    ...
}

Обратите внимание, что переменная loginComponent не аннотирована с помощью @Inject, поскольку вы не ожидаете, что эта переменная будет предоставлена Dagger.

Вы можете использовать ApplicationComponent, чтобы получить ссылку на LoginComponent, а затем внедрить LoginActivity следующим образом:

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 создается в методе onCreate() операции и неявно уничтожается при уничтожении операции.

LoginComponent должен всегда предоставлять один и тот же экземпляр LoginViewModel при каждом запросе. Для этого можно создать специальную область аннотаций и добавить в нее LoginComponent и LoginViewModel. Обратите внимание, что вы не можете использовать аннотацию @Singleton, поскольку она уже используется родительским компонентом, и это сделает объект синглтоном приложения (уникальным экземпляром для всего приложения). Вам нужно создать другую область аннотаций.

В этом случае вы могли бы назвать область действия @LoginScope, но это не рекомендуется. Название аннотации области действия не должно явно указывать на ее назначение. Вместо этого его следует назвать в зависимости от времени его жизни, поскольку аннотации могут повторно использоваться родственными компонентами, такими как RegistrationComponent и SettingsComponent. Поэтому вместо @LoginScope лучше использовать @ActivityScope.

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

Теперь, если у вас есть два фрагмента, которым нужен LoginViewModel, оба они получат один и тот же экземпляр. Например, если у вас есть LoginUsernameFragment и LoginPasswordFragment, их нужно внедрить с помощью LoginComponent:

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

Компоненты получают доступ к экземпляру компонента, который находится в объекте LoginActivity. Пример фрагмента кода для LoginUserNameFragment приведен ниже:

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

И то же самое для 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)
    }
}

На рисунке 3 показано, как выглядит граф Dagger с новым подкомпонентом. Классы с белой точкой (UserRepository, LoginRetrofitService и LoginViewModel) – это классы, у которых есть уникальный экземпляр, ограниченный соответствующими компонентами.

Граф приложения после добавления последнего подкомпонента

Рисунок 3. Представление графа, созданного для примера приложения Android

Давайте разберём части графика:

  1. NetworkModule (и, следовательно, LoginRetrofitService) включен в ApplicationComponent, поскольку вы указали его в компоненте.

  2. UserRepository остается в ApplicationComponent, так как относится к ApplicationComponent. Если проект растет, вы хотите использовать один и тот же экземпляр для разных функций (например, регистрации).

    Поскольку UserRepository является частью ApplicationComponent, его зависимости (т.е. UserLocalDataSource и UserRemoteDataSource) также должны быть в этом компоненте, чтобы можно было предоставить экземпляры UserRepository.

  3. LoginViewModel включен в LoginComponent, потому что он требуется только классам, внедренным LoginComponent. LoginViewModel не включен в ApplicationComponent, потому что ни одна зависимость в ApplicationComponent не требует LoginViewModel.

    Аналогично, если бы вы не ограничили область действия UserRepository до ApplicationComponent, Dagger автоматически включил бы UserRepository и его зависимости в LoginComponent, поскольку в настоящее время это единственное место, где используется UserRepository.

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

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

Рекомендации по созданию графа Dagger

При создании графа Dagger для приложения:

  • При создании компонента следует учитывать, какой элемент отвечает за время жизни этого компонента. В этом случае класс Application отвечает за ApplicationComponent, а LoginActivity – за LoginComponent.

  • Используйте области действия только там, где это имеет смысл. Чрезмерное использование областей видимости может негативно повлиять на производительность приложения во время выполнения: объект находится в памяти до тех пор, пока компонент находится в памяти, а получение объекта с заданной областью видимости обходится дороже. Когда Dagger предоставляет объект, он использует блокировку DoubleCheck вместо поставщика типа фабрики.

Тестирование проекта, в котором используется Dagger

Одно из преимуществ использования фреймворков внедрения зависимостей, таких как Dagger, заключается в том, что они упрощают тестирование кода.

Модульные тесты

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

Например, при тестировании LoginViewModel:

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

Сквозные тесты

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

Это требует более тщательного проектирования модулей в приложении. Тестовый компонент расширяет рабочий компонент и устанавливает другой набор модулей.

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 – это поддельная реализация оригинального NetworkModule. Там вы можете предоставить поддельные экземпляры или макеты того, что хотите заменить.

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

В интеграционных или сквозных тестах вы будете использовать TestApplication, который создает TestApplicationComponent вместо ApplicationComponent.

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

Затем это тестовое приложение используется в специальном TestRunner, который вы будете использовать для запуска инструментальных тестов. Дополнительную информацию можно найти в практической работе Использование Dagger в приложении для Android.

Работа с модулями Dagger

Модули Dagger позволяют инкапсулировать способ предоставления объектов семантическим способом. Вы можете включать модули в компоненты, а также в другие модули. Это мощный инструмент, но его легко использовать неправильно.

После добавления модуля в компонент или другой модуль он уже находится в графе Dagger. Dagger может предоставлять эти объекты в этом компоненте. Прежде чем добавлять модуль, проверьте, является ли он уже частью графа Dagger. Для этого посмотрите, добавлен ли он в компонент, или скомпилируйте проект и проверьте, может ли Dagger найти необходимые зависимости для этого модуля.

Рекомендуется объявлять модули в компоненте только один раз (за исключением особых случаев использования Dagger).

Предположим, вы настроили график следующим образом: ApplicationComponent включает Module1 и Module2, а Module1 включает 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 { ... }

Если теперь Module2 зависит от классов, предоставляемых ModuleX. Не рекомендуется включать ModuleX в Module2, поскольку в этом случае ModuleX будет дважды включен в граф, как показано в следующем фрагменте кода:

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

Вместо этого выполните одно из следующих действий:

  1. Проведите рефакторинг модулей и извлеките общий модуль в компонент.
  2. Создайте новый модуль с объектами, которые используются в обоих модулях, и извлеките его в компонент.

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

Рекомендуемый способ (вариант 1). ModuleX объявляется в графе Dagger один раз.

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

Рекомендуемый вариант 2. Общие зависимости из Module1 и Module2 в ModuleX извлекаются в новый модуль ModuleXCommon, который включается в компонент. Затем создаются два других модуля с именами ModuleXWithModule1Dependencies и ModuleXWithModule2Dependencies с зависимостями, специфичными для каждого модуля. Все модули объявляются в графе Dagger один раз.

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

Внедрение с помощью ИИ

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

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

Заключение

Если вы ещё этого не сделали, ознакомьтесь с рекомендациями. Чтобы узнать, как использовать Dagger в приложении для Android, ознакомьтесь с практической работой.