Pull to refresh

Dagger 2. Часть вторая. Custom scopes, Component dependencies, Subcomponents

Reading time 8 min
Views 118K

Всем привет!
Продолжаем наш цикл статей о Dagger 2. Если вы еще не ознакомились с первой частью, немедленно сделайте это :)
Большое спасибо за отзывы и комментарии по первой части.
В данной статье мы поговорим о custom scopes, о связывании компонентов через component dependencies и subcomponents. А также затронем такой немаловажный вопрос, как архитектура мобильного приложения, и как Dagger 2 помогает нам выстраивать более правильную, модульнонезависимую архитектуру.
Всем заинтересовавшихся прошу под кат!


Архитектура и custom scopes


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


  1. Clean Architecture от дядюшки Боба
  2. Clean Architecture в Android
  3. Перевод на русский

Подход Clean Architecture при построении архитектуры мне очень нравится. Он позволяет производить четкое вертикальное и горизонтальное построение всех модулей, где каждый класс делает только то, что он должен делать. Например, Fragment ответственнен только за отображение UI, а не осуществление запросов в сеть, БД, реализации бизнес-логики и прочего, что делало Fragment просто огромным куском запутанного кода. Думаю, многим это знакомо..


Рассмотрим пример. Есть приложение. В приложении есть несколько модулей, один из которых модуль чата. Модуль чата включает в себя три экрана: экран одиночного чата, группового чата и настройки.
Вспоминая Clean architecture, выделяем три горизонтальных уровня:


  1. Уровень всего приложения. Здесь находятся объекты, которые необходимы на протяжении всего жизненного цикла приложения, то есть "глобальные синглтоны". Пускай это будут объекты: Context (глобальный контекст), RxUtilsAbs (класс-утилита), NetworkUtils (класс-утилита) и IDataRepository (класс, отвечающий за запросы к серверу).
  2. Уровень чата. Объекты, которые нужны для всех трех экранов Чата: IChatInteractor (класс, реализующий конкретные бизнес-кейсы Чата) и IChatStateController (класс, отвечающий за состояние Чата).
  3. Уровень каждого экрана чата. У каждого экрана будет свой Presenter, устойчивый к переориентации, то есть чей жизненный цикл будет отличаться от жизненного цикла фрагмента/активити.

Схематично жизненные циклы будут выглядеть следующим образом:
image


Помните, в прошлой статье мы упоминали о "локальных" синглтонах? Так вот, объекты уровней чата и каждого экрана чата и представляют собой "локальные синглтоны", то есть объекты, чей жизненный цикл больше жизненного цикла стандартного активити/фрагмента, но меньше жизненного цикла всего приложения.
А вот теперь в дело вступает Dagger 2, у которого есть замечательный механизм Scopes. Данный механизм берет на себя создание и хранение единственного экземпляра необходимого класса до тех пор, пока соответствующий scope существует. Уверен, что фраза "пока существующий scope существует" несколько смущает и порождает вопросы. Не бойтесь, все прояснится ниже.
В прошлой статье мы помечали "глобальные синглтоны" scope @Singleton. Этот scope существовал все время жизни приложения. Но также мы можем создавать и свои custom scope-аннотации. Например:


@Scope
@Retention(RetentionPolicy.RUNTIME)
public @interface ChatScope {
}

А создание аннотации @Singleton в Dagger 2 выглядит так:


@Scope
@Retention(RetentionPolicy.RUNTIME)
public @interface Singleton {
}

То есть @Singleton от @ChatScope ничем не отличается, просто аннотация @Singleton предоставляется библиотекой по-умолчанию. И назначение этих аннотаций одно — указать Даггеру, провайдить "scope" или " unscoped" объекты. Но, снова повторюсь, за жизненный цикл "scope" объектов отвечаем мы.
Возвращаемся к нашему примеру. По текущей архитектуре у нас получаются три группы объектов, у которых своя "длина жизни". Таким образом, нам необходима три scope-аннотации:


  1. @Singleton — для глобальных синглтонов.
  2. @ChatScope — для объектов Чата.
  3. @ChatScreenScope — для объектов конкретного экрана Чата.

При этом отметим, что @ChatScope объекты должны иметь доступ к @Singleton объектам, а @ChatScreenScope — к @Singleton и @ChatScope объектам.
Схематично:
image
Далее напрашивается создание и соответствующих Компонент Даггера:


  1. AppComponent, который предоставляет "глобальные синглтоны".
  2. ChatComponent, предоставляющий "локальные синглтоны" для всех экранов Чата.
  3. SCComponent, предоставляющий "локальные синглтоны" для конкретного экрана Чата (SingleChatFragment, то есть экрана Одиночного чата).

И снова визуализируем вышеописанное:
image
В итоге получаем три компонента с тремя разными scope-аннотациями, которые связаны друг с другом по цепочке. ChatComponent зависит от AppComponent, а SCComponent — от ChatComponent.


Но теперь встает вопрос, как нам правильно связать эти компоненты? Существует два способа.


Component dependencies


Данный способ связи перекачивал из Dagger 1.
Отметим сразу особенности Component dependencies:


  1. Два зависимых компонента не могут иметь одинаковый scope. Подобнее тут.
  2. Родительский компонент в своем интерфейсе должен явно задавать объекты, которыми могут пользоваться зависимые компоненты.
  3. Компонент может зависеть от нескольких компонент.

Как в нашем примере будет выглядеть диаграмма зависимостей с component dependencies:
image
Теперь рассмотрим каждый компонент с его модулями по отдельности.


AppComponent
image
Обратим внимание, что в интерфейсе компонента мы явно задаем те объекты, которые будут доступны для дочерних компонент (но не для дочек дочерних компонент, об этой ситуации поговорим чуть позже). Например, если дочерний компонент захочет NetworkUtils, то Даггер выдаст соответствующую ошибку.
В интерфейсе мы также можем по-прежнему задавать и цели инъекций. То есть у вас не должно создастся заблуждение, что если компонент имеет дочерние компоненты, то он не может инъецировать свои зависимости в необходимые классы (активити/фрагменты/другое).


ChatComponent
image
В аннотации у ChatComponent мы явно прописываем, от какого компонента должен зависеть ChatComponent (зависит от AppComponent). Да, как уже отмечалось ранее, родителей у компонента может быть несколько (достаточно просто добавить в аннотацию новые компоненты-родители). А вот scope-аннотации компонент должны отличаться. И также в интерфейсе явно прописываем те объекты, к которым могут иметь доступ дочерние компоненты.
Обратили внимание на зеленую стрелочку? Как мы уже говорили, ChatComponent может использовать зависимости от AppComponent, которые тот явно указал. Но вот дочерние компоненты ChatComponent уже не смогут использовать зависимости от AppComponent, если только мы эти зависимости явно не пропишем в ChatComponent, что собственно и сделали для Context.


SCComponent
image
SCComponent является зависимым от ChatComponent, и он инъецирует зависимости в SingleChatFragment. При этом в SingleChatFragment данный компонент может инъецировать как SCPresenter, так и другие объекты родительских компонент, явно прописанные в соответствующих интерфейсах.


Остался последний шаг. Это проинициализировать компоненты:
image


По сравнению с обычным компонентом, при инициализации зависимого компонента в билдерах DaggerChatComponent и DaggerSCComponent появляется еще один метод — appComponent(...) (для DaggerChatComponent) и chatComponent(...) (для DaggerSCComponent), в которые мы указываем проинициализированные родительские компоненты.
Кстати, если у компонента два родителя, то в билдере появляются два соответствующих метода. Если три родителя, то и три метода и т.д.
Так как все компоненты у нас имеют свой жизненный цикл, отличный от жизненного цикла активити/фрагмента, то инициализацию и хранение экземпляров компонент мы произведем в Application файле. Пример Application классе рассмотрим в конце.


Subcomponents


Фича уже Dagger2.
Особенности:


  1. Необходимо прописывать в интерфейсе родителя метод получения Сабкомпонента (упрощенное название Subcomponent)
  2. Для Сабкомпонента доступны все объекты родителя
  3. Родитель может быть только один

Да, у Subcomponents есть некоторые отличия от Component dependencies. Рассмотрим схему и код, чтобы лучше понять различия.


image


По схеме видим, что для дочернего компонента доступны все объекты родителя, и так по всему дереву зависимостей компонент. Например, для SCComponent доступен NetworkUtils.


AppComponent


image


Следующее отличие Subcomponents. В интерфейсе AppComponent создаем метод для последующей инициализации ChatComponent. Опять-таки главное в этом методе — возвращаемое значение (ChatComponent) и аргументы (ChatModule).
Замечу, что в конструктор нашего ChatModule ничего передавать не нужно (конструктор по-умолчанию), поэтому в методе plusChatComponent можно и опустить данный аргумент. Однако для более ясной картины зависимостей и в образовательных целях пока оставим все максимально подробно.


ChatComponent
image
ChatComponent — является одновременно и дочерним и родительским компонентом. То, что он родительский, указывает метод создания SCComponent в интерфейсе. А то, что компонент является дочерним, указывает аннотация @Subcomponent.


SCComponent
image


Как мы отмечали ранее, так как все компоненты у нас имеют свой жизненный цикл, отличный от жизненного цикла активити/фрагмента, то инициализацию и хранение экземпляров компонент мы произведем в Application файле:


public class MyApp extends Application {

    protected static MyApp instance;

    public static MyApp get() {
        return instance;
    }

    // Dagger 2 components
    private AppComponent appComponent;
    private ChatComponent chatComponent;
    private SCComponent scComponent;

    @Override
    public void onCreate() {
        super.onCreate();
        instance = this;
        // init AppComponent on start of the Application
        appComponent = DaggerAppComponent.builder()
                .appModule(new AppModule(instance))
                .build();
    }

    public ChatComponent plusChatComponent() {
        // always get only one instance
        if (chatComponent == null) {
            // start lifecycle of chatComponent
            chatComponent = appComponent.plusChatComponent(new ChatModule());
        }
        return chatComponent;
    }

    public void clearChatComponent() {
        // end lifecycle of chatComponent
        chatComponent = null;
    }

    public SCComponent plusSCComponent() {
        // always get only one instance
        if (scComponent == null) {
            // start lifecycle of scComponent
            scComponent = chatComponent.plusSComponent(new SCModule());
        }
        return scComponent;
    }

    public void clearSCComponent() {
        // end lifecycle of scComponent
        scComponent = null;
    }

}

А вот теперь мы наконец-то можем увидеть жизненный цикл компонент в коде. Про AppComponent все понятно, мы его проинициализировали при старте приложения и больше не трогаем. А вот ChatComponent и SCComponent мы инициализируем по мере необходимости с помощью методов plusChatComponent() и plusSCComponent. Эти методы также отвечают за возврат единственных экземпляров компонент.
Так при повторном вызове, например,
scComponent = chatComponent.plusSComponent(new SCModule());
формируется новый экземпляр SCComponent со своим графом зависимостей.
С помощью методов clearChatComponent() и clearSCComponent() мы можем прекратить жизнь соответствующих компонент с их графами. Да, обычным занулением ссылок. Если снова необходимы ChatComponent и SCComponent, то мы просто вызываем методы plusChatComponent() и plusSCComponent, которые создают новые экземпляры.
На всякий случай уточню, что в данном примере инициализировать SCComponent, когда не проинициализирован ChatComponent мы не сможем, выхватим NullPointerException.
Также отмечу, что если у вас много компонентов и сабкомпонентов, то лучше вынести весь этот код с MyApp в специальный синглтон (например, Injector), который как раз будет ответственен за создание, уничтожение и предоставление необходимых Даггеровских компонент.


На этом все. Как вы увидели, custom scopes, component dependencies и subcomponent — крайне важные элементы Dagger 2, с помощью которых разработчик может создавать более структурированную и правильную архитектуру.
Дополнительно к прочтению рекомендую следующие статьи:


  1. Очень хорошая статья про Dagger 2 в общем
  2. Про custom scopes того же автора
  3. Отличия component dependencies от subcomponents

Буду рад вашим комментариям, замечаниям, вопросам и лайкам :)
В следующей статье мы рассмотрим применение Dagger 2 в тестировании, а также дополнительные, но от этого не менее важные и функциональные фичи библиотеки.
Третья часть — Dagger 2. Часть третья. Новые грани возможного.

Tags:
Hubs:
+10
Comments 26
Comments Comments 26

Articles