Всем привет!
Продолжаем наш цикл статей о 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-аннотации компонент должны отличаться. И также в интерфейсе явно прописываем те объекты, к которым могут иметь доступ дочерние компоненты.
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
). В аргументы вы помещаете все модули дочернего компонента. То есть, если бы в ChatComponent
было, скажем, шесть модулей, то все шесть пришлось бы указывать в аргументах.
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
.
На этом все. Как вы увидели, custom scopes, component dependencies и subcomponent — крайне важные элементы Dagger 2, с помощью которых разработчик может создавать более структурированную и правильную архитектуру.
Дополнительно к прочтению рекомендую следующие статьи:
- Очень хорошая статья про Dagger 2 в общем
- Про custom scopes того же автора
- Отличия component dependencies от subcomponents
Буду рад вашим комментариям, замечаниям, вопросам и лайкам :)
В следующей статье мы рассмотрим применение Dagger 2 в тестировании, а также дополнительные, но от этого не менее важные и функциональные фичи библиотеки.
Автор: xoxol_89