Всем привет!
Продолжаем наш цикл статей о Dagger 2. Если вы еще не ознакомились с первой частью, немедленно сделайте это :)
Большое спасибо за отзывы и комментарии по первой части.
В данной статье мы поговорим о custom scopes, о связывании компонентов через component dependencies и subcomponents. А также затронем такой немаловажный вопрос, как архитектура мобильного приложения, и как Dagger 2 помогает нам выстраивать более правильную, модульнонезависимую архитектуру.
Всем заинтересовавшихся прошу под кат!
Архитектура и custom scopes
Начнем с архитектуры. В последнее время этому вопросу уделяется много внимания, посвящается много статей и выступлений. Вопрос, безусловно, важный, ведь от того, как мы лодку назовем, так она и поплывет. Поэтому я очень рекомендую для начала ознакомиться с этими статьями:
Подход Clean Architecture при построении архитектуры мне очень нравится. Он позволяет производить четкое вертикальное и горизонтальное построение всех модулей, где каждый класс делает только то, что он должен делать. Например, Fragment ответственнен только за отображение UI, а не осуществление запросов в сеть, БД, реализации бизнес-логики и прочего, что делало Fragment просто огромным куском запутанного кода. Думаю, многим это знакомо..
Рассмотрим пример. Есть приложение. В приложении есть несколько модулей, один из которых модуль чата. Модуль чата включает в себя три экрана: экран одиночного чата, группового чата и настройки.
Вспоминая Clean architecture, выделяем три горизонтальных уровня:
- Уровень всего приложения. Здесь находятся объекты, которые необходимы на протяжении всего жизненного цикла приложения, то есть "глобальные синглтоны". Пускай это будут объекты:
Context
(глобальный контекст),RxUtilsAbs
(класс-утилита),NetworkUtils
(класс-утилита) иIDataRepository
(класс, отвечающий за запросы к серверу). - Уровень чата. Объекты, которые нужны для всех трех экранов Чата:
IChatInteractor
(класс, реализующий конкретные бизнес-кейсы Чата) иIChatStateController
(класс, отвечающий за состояние Чата). - Уровень каждого экрана чата. У каждого экрана будет свой Presenter, устойчивый к переориентации, то есть чей жизненный цикл будет отличаться от жизненного цикла фрагмента/активити.
Схематично жизненные циклы будут выглядеть следующим образом:
Помните, в прошлой статье мы упоминали о "локальных" синглтонах? Так вот, объекты уровней чата и каждого экрана чата и представляют собой "локальные синглтоны", то есть объекты, чей жизненный цикл больше жизненного цикла стандартного активити/фрагмента, но меньше жизненного цикла всего приложения.
А вот теперь в дело вступает 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-аннотации:
@Singleton
— для глобальных синглтонов.@ChatScope
— для объектов Чата.@ChatScreenScope
— для объектов конкретного экрана Чата.
При этом отметим, что @ChatScope
объекты должны иметь доступ к @Singleton
объектам, а @ChatScreenScope
— к @Singleton
и @ChatScope
объектам.
Схематично:
Далее напрашивается создание и соответствующих Компонент Даггера:
AppComponent
, который предоставляет "глобальные синглтоны".ChatComponent
, предоставляющий "локальные синглтоны" для всех экранов Чата.SCComponent
, предоставляющий "локальные синглтоны" для конкретного экрана Чата (SingleChatFragment
, то есть экрана Одиночного чата).
И снова визуализируем вышеописанное:
В итоге получаем три компонента с тремя разными scope-аннотациями, которые связаны друг с другом по цепочке. ChatComponent
зависит от AppComponent
, а SCComponent
— от ChatComponent
.
Но теперь встает вопрос, как нам правильно связать эти компоненты? Существует два способа.
Component dependencies
Данный способ связи перекачивал из Dagger 1.
Отметим сразу особенности Component dependencies:
- Два зависимых компонента не могут иметь одинаковый scope. Подобнее тут.
- Родительский компонент в своем интерфейсе должен явно задавать объекты, которыми могут пользоваться зависимые компоненты.
- Компонент может зависеть от нескольких компонент.
Как в нашем примере будет выглядеть диаграмма зависимостей с component dependencies:
Теперь рассмотрим каждый компонент с его модулями по отдельности.
AppComponent
Обратим внимание, что в интерфейсе компонента мы явно задаем те объекты, которые будут доступны для дочерних компонент (но не для дочек дочерних компонент, об этой ситуации поговорим чуть позже). Например, если дочерний компонент захочет NetworkUtils
, то Даггер выдаст соответствующую ошибку.
В интерфейсе мы также можем по-прежнему задавать и цели инъекций. То есть у вас не должно создастся заблуждение, что если компонент имеет дочерние компоненты, то он не может инъецировать свои зависимости в необходимые классы (активити/фрагменты/другое).
ChatComponent
В аннотации у ChatComponent
мы явно прописываем, от какого компонента должен зависеть ChatComponent
(зависит от AppComponent
). Да, как уже отмечалось ранее, родителей у компонента может быть несколько (достаточно просто добавить в аннотацию новые компоненты-родители). А вот scope-аннотации компонент должны отличаться. И также в интерфейсе явно прописываем те объекты, к которым могут иметь доступ дочерние компоненты.
Обратили внимание на зеленую стрелочку? Как мы уже говорили, ChatComponent
может использовать зависимости от AppComponent
, которые тот явно указал. Но вот дочерние компоненты ChatComponent
уже не смогут использовать зависимости от AppComponent
, если только мы эти зависимости явно не пропишем в ChatComponent
, что собственно и сделали для Context
.
SCComponent
SCComponent
является зависимым от ChatComponent
, и он инъецирует зависимости в SingleChatFragment
. При этом в SingleChatFragment
данный компонент может инъецировать как SCPresenter
, так и другие объекты родительских компонент, явно прописанные в соответствующих интерфейсах.
Остался последний шаг. Это проинициализировать компоненты:
По сравнению с обычным компонентом, при инициализации зависимого компонента в билдерах DaggerChatComponent
и DaggerSCComponent
появляется еще один метод — appComponent(...)
(для DaggerChatComponent
) и chatComponent(...)
(для DaggerSCComponent
), в которые мы указываем проинициализированные родительские компоненты.
Кстати, если у компонента два родителя, то в билдере появляются два соответствующих метода. Если три родителя, то и три метода и т.д.
Так как все компоненты у нас имеют свой жизненный цикл, отличный от жизненного цикла активити/фрагмента, то инициализацию и хранение экземпляров компонент мы произведем в Application файле. Пример Application классе рассмотрим в конце.
Subcomponents
Фича уже Dagger2.
Особенности:
- Необходимо прописывать в интерфейсе родителя метод получения Сабкомпонента (упрощенное название Subcomponent)
- Для Сабкомпонента доступны все объекты родителя
- Родитель может быть только один
Да, у Subcomponents есть некоторые отличия от Component dependencies. Рассмотрим схему и код, чтобы лучше понять различия.
По схеме видим, что для дочернего компонента доступны все объекты родителя, и так по всему дереву зависимостей компонент. Например, для SCComponent
доступен NetworkUtils
.
AppComponent
Следующее отличие Subcomponents. В интерфейсе AppComponent
создаем метод для последующей инициализации ChatComponent
. Опять-таки главное в этом методе — возвращаемое значение (ChatComponent
) и аргументы (ChatModule
).
Замечу, что в конструктор нашего ChatModule
ничего передавать не нужно (конструктор по-умолчанию), поэтому в методе plusChatComponent
можно и опустить данный аргумент. Однако для более ясной картины зависимостей и в образовательных целях пока оставим все максимально подробно.
ChatComponent
ChatComponent
— является одновременно и дочерним и родительским компонентом. То, что он родительский, указывает метод создания SCComponent
в интерфейсе. А то, что компонент является дочерним, указывает аннотация @Subcomponent
.
SCComponent
Как мы отмечали ранее, так как все компоненты у нас имеют свой жизненный цикл, отличный от жизненного цикла активити/фрагмента, то инициализацию и хранение экземпляров компонент мы произведем в 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, с помощью которых разработчик может создавать более структурированную и правильную архитектуру.
Дополнительно к прочтению рекомендую следующие статьи:
- Очень хорошая статья про Dagger 2 в общем
- Про custom scopes того же автора
- Отличия component dependencies от subcomponents
Буду рад вашим комментариям, замечаниям, вопросам и лайкам :)
В следующей статье мы рассмотрим применение Dagger 2 в тестировании, а также дополнительные, но от этого не менее важные и функциональные фичи библиотеки.
Третья часть — Dagger 2. Часть третья. Новые грани возможного.