Всем привет!
Продолжаем наш цикл статей о Dagger 2. Если вы еще не ознакомились с первой частью, немедленно сделайте это :)
Большое спасибо за отзывы и комментарии по первой части.
В данной статье мы поговорим о custom scopes, о связывании компонентов через component dependencies и subcomponents. А также затронем такой немаловажный вопрос, как архитектура мобильного приложения, и как Dagger 2 помогает нам выстраивать более правильную, модульнонезависимую архитектуру.
Всем заинтересовавшихся прошу под кат!
Начнем с архитектуры. В последнее время этому вопросу уделяется много внимания, посвящается много статей и выступлений. Вопрос, безусловно, важный, ведь от того, как мы лодку назовем, так она и поплывет. Поэтому я очень рекомендую для начала ознакомиться с этими статьями:
Подход Clean Architecture при построении архитектуры мне очень нравится. Он позволяет производить четкое вертикальное и горизонтальное построение всех модулей, где каждый класс делает только то, что он должен делать. Например, Fragment ответственнен только за отображение UI, а не осуществление запросов в сеть, БД, реализации бизнес-логики и прочего, что делало Fragment просто огромным куском запутанного кода. Думаю, многим это знакомо..
Рассмотрим пример. Есть приложение. В приложении есть несколько модулей, один из которых модуль чата. Модуль чата включает в себя три экрана: экран одиночного чата, группового чата и настройки.
Вспоминая Clean architecture, выделяем три горизонтальных уровня:
Схематично жизненные циклы будут выглядеть следующим образом:

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

Далее напрашивается создание и соответствующих Компонент Даггера:
И снова визуализируем вышеописанное:

В итоге получаем три компонента с тремя разными scope-аннотациями, которые связаны друг с другом по цепочке.
Но теперь встает вопрос, как нам правильно связать эти компоненты? Существует два способа.
Данный способ связи перекачивал из Dagger 1.
Отметим сразу особенности Component dependencies:
Как в нашем примере будет выглядеть диаграмма зависимостей с component dependencies:

Теперь рассмотрим каждый компонент с его модулями по отдельности.
AppComponent

Обратим внимание, что в интерфейсе компонента мы явно задаем те объекты, которые будут доступны для дочерних компонент (и для дочек дочерних компонент). Например, если дочерний компонент захочет
ChatComponent

В аннотации у
SCComponent

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

По сравнению с обычным компонентом, при инициализации зависимого компонента в билдерах
Кстати, если у компонента два родителя, то в билдере появляются два соответствующих метода. Если три родителя, то и три метода и т.д.
Так как все компоненты у нас имеют свой жизненный цикл, отличный от жизненного цикла активити/фрагмента, то инициализацию и хранение экземпляров компонент мы произведем в Application файле. Пример Application классе рассмотрим в конце.
Фича уже Dagger2.
Особенности:
Да, у Subcomponents есть некоторые отличия от Component dependencies. Рассмотрим схему и код, чтобы лучше понять различия.

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

Следующее отличие Subcomponents. В интерфейсе
ChatComponent

SCComponent

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