Основы Dagger

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

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

Преимущества использования Dagger

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

  • Генерирует код AppContainer (граф приложения), который вы вручную реализовали в разделе ручного внедрения зависимостей.

  • Создание фабрик для классов, доступных в графе приложения. Вот как зависимости удовлетворяются внутри.

  • Решение о том, следует ли повторно использовать зависимость или создать новый экземпляр с помощью областей.

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

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

Во время сборки Dagger просматривает ваш код и:

  • Создает и проверяет графики зависимостей, обеспечивая следующее:

    • Зависимости каждого объекта могут быть удовлетворены, поэтому исключений во время выполнения не возникает.
    • Циклы зависимостей отсутствуют, поэтому бесконечные циклы невозможны.
  • Генерирует классы, которые используются во время выполнения для создания фактических объектов и их зависимостей.

Простой пример использования Dagger: создание фабрики

Чтобы продемонстрировать, как работать с Dagger, давайте создадим простую фабрику для класса UserRepository, показанного на следующей диаграмме:

Определите UserRepository следующим образом:

Kotlin

class UserRepository(
    private val localDataSource: UserLocalDataSource,
    private val remoteDataSource: UserRemoteDataSource
) { ... }

Java

public class UserRepository {

    private final UserLocalDataSource userLocalDataSource;
    private final UserRemoteDataSource userRemoteDataSource;

    public UserRepository(UserLocalDataSource userLocalDataSource, UserRemoteDataSource userRemoteDataSource) {
        this.userLocalDataSource = userLocalDataSource;
        this.userRemoteDataSource = userRemoteDataSource;
    }

    ...
}

Добавьте аннотацию @Inject в конструктор UserRepository, чтобы Dagger знал, как создать UserRepository:

Kotlin

// @Inject lets Dagger know how to create instances of this object
class UserRepository @Inject constructor(
    private val localDataSource: UserLocalDataSource,
    private val remoteDataSource: UserRemoteDataSource
) { ... }

Java

public class UserRepository {

    private final UserLocalDataSource userLocalDataSource;
    private final UserRemoteDataSource userRemoteDataSource;

    // @Inject lets Dagger know how to create instances of this object
    @Inject
    public UserRepository(UserLocalDataSource userLocalDataSource, UserRemoteDataSource userRemoteDataSource) {
        this.userLocalDataSource = userLocalDataSource;
        this.userRemoteDataSource = userRemoteDataSource;
    }
}

В приведенном выше фрагменте кода вы сообщаете Dagger:

  1. Как создать экземпляр UserRepository с помощью конструктора с аннотацией @Inject.

  2. Зависимости: UserLocalDataSource и UserRemoteDataSource.

Теперь Dagger знает, как создать экземпляр UserRepository, но не знает, как создать его зависимости. Если вы добавите аннотации и к другим классам, Dagger будет знать, как их создавать:

Kotlin

// @Inject lets Dagger know how to create instances of these objects
class UserLocalDataSource @Inject constructor() { ... }
class UserRemoteDataSource @Inject constructor() { ... }

Java

public class UserLocalDataSource {
    @Inject
    public UserLocalDataSource() { }
}

public class UserRemoteDataSource {
    @Inject
    public UserRemoteDataSource() { }
}

Компоненты Dagger

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

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

Kotlin

// @Component makes Dagger create a graph of dependencies
@Component
interface ApplicationGraph {
    // The return type  of functions inside the component interface is
    // what can be provided from the container
    fun repository(): UserRepository
}

Java

// @Component makes Dagger create a graph of dependencies
@Component
public interface ApplicationGraph {
    // The return type  of functions inside the component interface is
    // what can be consumed from the graph
    UserRepository userRepository();
}

При сборке проекта Dagger генерирует реализацию интерфейса ApplicationGraph: DaggerApplicationGraph. С помощью инструмента обработки аннотаций Dagger создает граф зависимостей, который состоит из отношений между тремя классами (UserRepository, UserLocalDatasource и UserRemoteDataSource) и имеет только одну точку входа – получение экземпляра UserRepository. Вот как это сделать:

Kotlin

// Create an instance of the application graph
val applicationGraph: ApplicationGraph = DaggerApplicationGraph.create()
// Grab an instance of UserRepository from the application graph
val userRepository: UserRepository = applicationGraph.repository()

Java

// Create an instance of the application graph
ApplicationGraph applicationGraph = DaggerApplicationGraph.create();

// Grab an instance of UserRepository from the application graph
UserRepository userRepository = applicationGraph.userRepository();

Dagger создает новый экземпляр UserRepository каждый раз, когда он запрашивается.

Kotlin

val applicationGraph: ApplicationGraph = DaggerApplicationGraph.create()

val userRepository: UserRepository = applicationGraph.repository()
val userRepository2: UserRepository = applicationGraph.repository()

assert(userRepository != userRepository2)

Java

ApplicationGraph applicationGraph = DaggerApplicationGraph.create();

UserRepository userRepository = applicationGraph.userRepository();
UserRepository userRepository2 = applicationGraph.userRepository();

assert(userRepository != userRepository2)

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

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

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

В нашем примере вы можете захотеть, чтобы в графе был доступен уникальный экземпляр UserRepository, чтобы каждый раз, когда вы запрашиваете UserRepository, вы всегда получали один и тот же экземпляр. Это полезно в вашем примере, потому что в реальном приложении с более сложным графом приложения у вас может быть несколько объектов ViewModel, зависящих от UserRepository, и вы не хотите создавать новые экземпляры UserLocalDataSource и UserRemoteDataSource каждый раз, когда необходимо предоставить UserRepository.

При ручном внедрении зависимостей вы делаете это, передавая один и тот же экземпляр UserRepository в конструкторы классов ViewModel. Но в Dagger, поскольку вы не пишете этот код вручную, вам нужно сообщить Dagger, что вы хотите использовать один и тот же экземпляр. Это можно сделать с помощью аннотаций области действия.

Области видимости в Dagger

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

Чтобы получить уникальный экземпляр UserRepository при запросе репозитория в ApplicationGraph, используйте одну и ту же аннотацию области действия для интерфейса @Component и UserRepository. Вы можете использовать аннотацию @Singleton, которая уже поставляется с пакетом javax.inject, используемым Dagger:

Kotlin

// Scope annotations on a @Component interface informs Dagger that classes annotated
// with this annotation (i.e. @Singleton) are bound to the life of the graph and so
// the same instance of that type is provided every time the type is requested.
@Singleton
@Component
interface ApplicationGraph {
    fun repository(): UserRepository
}

// Scope this class to a component using @Singleton scope (i.e. ApplicationGraph)
@Singleton
class UserRepository @Inject constructor(
    private val localDataSource: UserLocalDataSource,
    private val remoteDataSource: UserRemoteDataSource
) { ... }

Java

// Scope annotations on a @Component interface informs Dagger that classes annotated
// with this annotation (i.e. @Singleton) are scoped to the graph and the same
// instance of that type is provided every time the type is requested.
@Singleton
@Component
public interface ApplicationGraph {
    UserRepository userRepository();
}

// Scope this class to a component using @Singleton scope (i.e. ApplicationGraph)
@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;
    }
}

Вы также можете создать и использовать собственную аннотацию области действия. Вы можете создать аннотацию области действия следующим образом:

Kotlin

// Creates MyCustomScope
@Scope
@MustBeDocumented
@Retention(value = AnnotationRetention.RUNTIME)
annotation class MyCustomScope

Java

// Creates MyCustomScope
@Scope
@Retention(RetentionPolicy.RUNTIME)
public @interface MyCustomScope {}

После этого вы сможете использовать его как раньше:

Kotlin

@MyCustomScope
@Component
interface ApplicationGraph {
    fun repository(): UserRepository
}

@MyCustomScope
class UserRepository @Inject constructor(
    private val localDataSource: UserLocalDataSource,
    private val service: UserService
) { ... }

Java

@MyCustomScope
@Component
public interface ApplicationGraph {
    UserRepository userRepository();
}

@MyCustomScope
public class UserRepository {

    private final UserLocalDataSource userLocalDataSource;
    private final UserRemoteDataSource userRemoteDataSource;

    @Inject
    public UserRepository(UserLocalDataSource userLocalDataSource, UserRemoteDataSource userRemoteDataSource) {
        this.userLocalDataSource = userLocalDataSource;
        this.userRemoteDataSource = userRemoteDataSource;
    }
}

В обоих случаях объект предоставляется с той же областью действия, которая используется для аннотирования интерфейса @Component. Таким образом, каждый раз, когда вы вызываете applicationGraph.repository(), вы получаете один и тот же экземпляр UserRepository.

Kotlin

val applicationGraph: ApplicationGraph = DaggerApplicationGraph.create()

val userRepository: UserRepository = applicationGraph.repository()
val userRepository2: UserRepository = applicationGraph.repository()

assert(userRepository == userRepository2)

Java

ApplicationGraph applicationGraph = DaggerApplicationGraph.create();

UserRepository userRepository = applicationGraph.userRepository();
UserRepository userRepository2 = applicationGraph.userRepository();

assert(userRepository == userRepository2)

Заключение

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

На следующей странице вы узнаете, как добавить Dagger в приложение для Android.