- PVSM.RU - https://www.pvsm.ru -
Всем привет!
Продолжаем наш цикл статей о Dagger 2. Если вы еще не ознакомились с первой частью [1], немедленно сделайте это :)
Большое спасибо за отзывы и комментарии по первой части.
В данной статье мы поговорим о custom scopes, о связывании компонентов через component dependencies и subcomponents. А также затронем такой немаловажный вопрос, как архитектура мобильного приложения и как Dagger 2 помогает нам выстраивать более правильную, модульнонезависимую архитектуру.
Всем заинтересовавшихся прошу под кат!
Начнем с архитектуры. В последнее время этому вопросу уделяется много внимания, посвящается много статей и выступлений. Вопрос, безусловно, важный, ведь от того, как мы лодку назовем, так она и поплывет. Поэтому я очень рекомендую для начала ознакомиться с этими статьями:
Подход Clean Architecture при построении архитектуры мне очень нравится. Он позволяет производить четкое вертикальное и горизонтальное построение всех модулей, где каждый класс делает только то, что он должен делать. Например, Fragment ответственнен только за отображение UI, а не осуществление запросов в сеть, БД, реализации бизнес-логики и прочего, что делало Fragment просто огромным куском запутанного кода. Думаю, многим это знакомо..
Рассмотрим пример. Есть приложение. В приложении есть несколько модулей, один из которых модуль чата. Модуль чата включает в себя три экрана: экран одиночного чата, группового чата и настройки.
Вспоминая Clean architecture, выделяем три горизонтальных уровня:
Context (глобальный контекст), RxUtilsAbs (класс-утилита), NetworkUtils (класс-утилита) и IDataRepository (класс, отвечающий за запросы к серверу).IChatInteractor (класс, реализующий конкретные бизнес-кейсы Чата) и IChatStateController (класс, отвечающий за состояние Чата).
Схематично жизненные циклы будут выглядеть следующим образом:

Помните, в прошлой статье мы упоминали о "локальных" синглтонах? Так вот, объекты уровней чата и каждого экрана чата и представляют собой "локальные синглтоны", то есть объекты, чей жизненный цикл больше жизненного цикла стандартного активити/фрагмента, но меньше жизненного цикла всего приложения.
А вот теперь в дело вступает 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.
Но теперь встает вопрос, как нам правильно связать эти компоненты? Существует два способа.
Данный способ связи перекачивал из Dagger 1.
Отметим сразу особенности Component dependencies:
Как в нашем примере будет выглядеть диаграмма зависимостей с component dependencies:

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

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

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

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

По сравнению с обычным компонентом, при инициализации зависимого компонента в билдерах DaggerChatComponent и DaggerSCComponent появляется еще один метод — appComponent(...) (для DaggerChatComponent) и chatComponent(...) (для DaggerSCComponent), в которые мы указываем проинициализированные родительские компоненты.
Кстати, если у компонента два родителя, то в билдере появляются два соответствующих метода. Если три родителя, то и три метода и т.д.
Так как все компоненты у нас имеют свой жизненный цикл, отличный от жизненного цикла активити/фрагмента, то инициализацию и хранение экземпляров компонент мы произведем в Application файле. Пример Application файла рассмотрим в конце.
Фича уже Dagger2.
Особенности:
Да, у 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 в тестировании, а также дополнительные, но от этого не менее важные и функциональные фичи библиотеки.
Автор: xoxol_89
Источник [9]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/android-development/115927
Ссылки в тексте:
[1] первой частью: https://habrahabr.ru/post/279125/
[2] Clean Architecture от дядюшки Боба: http://blog.8thlight.com/uncle-bob/2012/08/13/the-clean-architecture.html
[3] Clean Architecture в Android: http://fernandocejas.com/2014/09/03/architecting-android-the-clean-way/
[4] Перевод на русский: https://habrahabr.ru/post/250659/
[5] тут: https://github.com/google/dagger/issues/107#issuecomment-71073298
[6] Очень хорошая статья про Dagger 2 в общем: http://frogermcs.github.io/dependency-injection-with-dagger-2-the-api/
[7] Про custom scopes того же автора: http://frogermcs.github.io/dependency-injection-with-dagger-2-custom-scopes/
[8] Отличия component dependencies от subcomponents: http://jellybeanssir.blogspot.ru/2015/05/component-dependency-vs-submodules-in.html
[9] Источник: https://habrahabr.ru/post/279641/
Нажмите здесь для печати.