Всем привет!
Продолжаем наш цикл статей о 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-аннотации компонент должны отличаться. И также в интерфейсе явно прописываем те объекты, к которым могут иметь доступ дочерние компоненты.

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). В аргументы вы помещаете все модули дочернего компонента. То есть, если бы в ChatComponent было, скажем, шесть модулей, то все шесть пришлось бы указывать в аргументах.

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.

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

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

Комментарии (3)