Dagger 2 для начинающих Android разработчиков. Dagger 2. Часть 1

Это третья статья цикла «Dagger 2 для начинающих Android разработчиков». Первую часть можно прочесть здесь, вторую - здесь.

Ранее в цикле статей

В последней статье мы поняли, что класс не должен создавать зависимости. Вместо этого, он должен получать их снаружи. Также мы рассмотрели простой пример внедрения зависимостей в действии. Взяли пример с битвой бастардов и попытались избавиться от сильных связей (hard dependencies) через внедрение зависимостей.

Как внедрение зависимостей может усложниться?

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

Чтобы проиллюстрировать описанную проблему, давайте немного усложним наш пример. Во время войны, в битве бастардов (BattleOfBastards) вероятно потребуется помощь союзников (Allies). Также железный банк (IronBank) будет финансировать дома. Измененный главный метод будет выглядеть следующим образом:

fun main() {
    val bank = IronBank()
    val allies = Allies(bank)
    val starks = Starks(allies, bank)
    val boltons = Boltons(allies, bank)

    val war = War(starks, boltons)
    war.prepare()
    war.report()
}

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

Dagger 2 спешит на помощь

Dagger 2 — это один из фреймворков с открытым исходным кодом для внедрения зависимостей (далее буду использовать DI, от Dependency Injection), который генерирует большое количество шаблонного кода за вас. Почему он лучше остальных? Сейчас это единственный DI фреймворк, который генерирует полностью отслеживаемый Java код, имитирующий тот код, который вы могли написать вручную. Это означает, что в построении графа зависимостей нет никакой магии. Dagger 2 менее динамичен, чем другие (в нем не используется рефлексия), но простота и производительность сгенерированного кода находятся на том же уровне, что и у написанного вручную. Коротко, Dagger 2 генерирует весь шаблонный код для внедрения зависимостей за вас.

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

Понимание обработчиков аннотаций (Annotation Processors)

Аннотации

Аннотации — это вид метаданных, который может быть связан с классами, методами, полями и даже другими аннотациями. Аннотации используются в Java для предоставления дополнительной информации, как альтернатива XML или маркерным интерфейсам (пустые интерфейсы). К аннотациям можно получить доступ и в процессе выполнения программы (runtime) через механизм рефлексии.

Обработчики аннотаций (Annotation Processors)

Обработчики аннотаций — это генераторы кода, которые скрывают от вас шаблонный код, создавая его за вас во время компиляции. Пока эти действия выполняются во время компиляции, никакого отрицательного влияния на производительность нет.

Почему я должен знать об обработчиках аннотаций?

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

Примеры

В Java вы часто можете видеть в классах аннотацию @Override. Если вы использовали Butterknife, @BindView — это тоже аннотация, которая скрывает за собой некоторые метаданные, помогающие генерировать код.

Аннотации Dagger 2

Рассмотрим некоторые аннотации Dagger 2 перед тем, как будем его использовать. Сейчас сосредоточимся на двух — @Inject и @Component.

@Inject

Это наиболее важная аннотация. JSR-330 определяет данную аннотацию как пометку для зависимостей, которые должны быть предоставлены фреймворком для внедрения зависимостей.

  • Внедрение в конструктор (Constructor Injection) — используется с конструктором класса.
  • Внедрение в поле (Field Injection) — используется с полями класса.
  • Внедрение в метод (Method Injection ) — используется с методами.
/*
* Примеры различного использования 
* аннотации Inject в Dagger
*/

class Starks @Inject constructor(
   allies: Allies
) {

    @Inject
    lateinit var allies: Allies

    @Inject
    fun prepareForWar(allies: Allies) {
        // что-то происходит
    }
}

Другими словами, аннотация @Inject сообщит Dagger, какие зависимости должны быть предоставлены зависимому объекту. Это как агенты железного банка, которые ведут переговоры с домами и определяют сумму кредита, которую могут предоставить дому.

@Component

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

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

Убить Белых Ходоков валирийским мечом

Давайте используем Dagger 2 для примера с битвой бастардов. В этом примере нужны две зависимости для класса WarStarks и Boltons.

Настройка Dagger 2

Для настройки Dagger 2 в IntelliJ Idea используйте build.gradle файл из ветки моего проекта. Также убедитесь в том, что обработка аннотаций подключена (File -> Settings -> Build, execution and deployment -> Compiler -> Annotation Processing -> Enable annotation processing (флаг должен быть установлен)). Не забудьте отметить и это: File -> Settings -> Build, execution and deployment -> Gradle -> Runner ->Delegate IDE build/run actions to cradle.

Добавление аннотации @Inject

План — внедрить зависимости Starks и Boltons в класс War с помощью Dagger 2. О чём мы должны явно ему сказать. Ниже пример того, как это нужно делать, используя внедрение с использованием конструктора.

class Boltons @Inject constructor() : House {

    override fun prepareForWar() {
        //что-то происходит
        println("${this::class.simpleName} prepared for war")
    }

    override fun reportForWar() {
        //что-то происходит
        println("${this::class.simpleName} reporting...")
    }
}
class Starks @Inject constructor() : House {

    override fun prepareForWar() {
        //что-то происходит
        println("${this::class.simpleName} prepared for war")
    }

    override fun reportForWar() {
        //что-то происходит
        println("${this::class.simpleName} reporting...")
    }
}

Эти две зависимости используются в конструкторе класса War, где мы должны это отметить.

План заключается в том, чтобы сделать зависимость или объект класса War
доступным всем другим классам. Но для работы класса War необходимо предоставить ему два класса, от которых он зависит, — Starks и Boltons.

class War @Inject constructor(
    private val starks: Starks,
    private val boltons: Boltons
) {

    @Inject
    public War(Starks starks, Boltons bolton){
        this.starks = starks;
        this.boltons = bolton;
    }

    public void prepare(){
        starks.prepareForWar();
        boltons.prepareForWar();
    }

    public void report(){
        starks.reportForWar();
        boltons.reportForWar();
    }
}

Добавление аннотации @Component

Как мы узнали ранее, @Component — это мост между генерируемым кодом и зависимостями. Также @Component говорит Dagger 2, как необходимо внедрять зависимость. Сделаем интерфейс BattleComponent внутри класса BattleOfBastards (можно сделать и отдельно).

@Component
interface BattleComponent {
    fun getWar(): War
}

Этот интерфейс будет реализован классом, который сгенерирует Dagger 2, а функция getWar() вернет экземпляр War, который мы сможем использовать в подходящем месте.

Теперь необходимо пересобрать (rebuild) проект!

После пересборки проекта вы увидите, что Dagger 2 сгенерировал класс под названием DaggerBattleComponent — он поможет нам внедрить класс War. Используем этот класс для получения экземпляра War.

fun main() {
    //Ручное внедрение зависимостей
    //val starks = Starks()
    //val boltons = Boltons()
    //val war = War(starks, boltons)
    //war.prepare()
    //war.report()

    //Использование Dagger 2
    val component = DaggerBattleComponent.create()
    val war = component.getWar()
    war.prepare()
    war.report()
}

Используя класс DaggerBattleComponent, мы можем вызвать метод getWar(), который возвращает экземпляр War. В этом экземпляре уже будут внедрены зависимости Starks и Boltons.

Поздравляю! Вы создали первый проект с использованием Dagger 2. Я очень ценю то, что вы нашли на это время и зашли так далеко. Время отпраздновать.

Резюме

Мы обсудили, как ручное использование DI усложняет работу и увеличивает количество шаблонного кода. Далее обсудили, как Dagger 2 помогает нам избавиться от этой боли и сам генерирует шаблонный код.

После разобрали информацию по обработчикам аннотаций и базовым аннотациям Dagger 2 (@Inject и @Component). Затем применили аннотации в нашем примере и внедрили зависимости, используя Dagger 2.

Что дальше?

В следующей статье мы поработаем с классами, которые генерирует Dagger 2 и рассмотрим другие аннотации этого фреймворка.

Источник: Dagger 2 для начинающих Android разработчиков. Dagger 2. Часть 1