Всем привет!
В последнее время появилось много средств, библиотек, которые существенно облегчают написание кода под Android. Только успевай за всем следить и все пробовать. Одним из таких средств является библиотека Dagger 2.
В сети уже много различного материала, посвященного данной библиотеке. Но когда я только начинал ознакамливаться с Dagger 2, читал статьи, смотрел доклады, я во всем этом находил один общий недостаток — мне, как человеку, не работавшему со Springом и прочими подобными библиотеками, было довольно сложно понять, откуда берутся зависимости, как они "провайдятся" и что вообще там происходит. На слушателей/читателей обычно сразу "вываливается" большое количество кода с новыми аннотациями. И это как-то работало. В итоге, после доклада/статьи в голове все никак не могло сложиться в единую понятную картину.
Сейчас, оглядываясь назад, я понимаю, что мне тогда очень не хватало схематичного отображения, картинок, явно показывающих "что, откуда и куда". Поэтому в своем цикле статей я постараюсь восполнить данный пробел. Надеюсь, это поможет новичкам и всем заинтересованным лучше понять Dagger 2 и решиться попробовать его у себя в проекте. Могу сразу сказать, это стоит того :)
И да, изначально я хотел написать одну статью, но материала и картинок вышло уж как-то много, поэтому информацию я буду выкладывать небольшими порциями, чтобы читатель мог постепенно погружаться в тему.
Теория
Быстро пробежимся по теоретическим аспектам.
Dagger 2 представляет собой библиотеку, которая помогает разработчику реализовать паттерн Внедрение зависимости (Dependency Injection), который в свою очередь является "специфичной формой инверсии управления (Inversion of control)".
Принципы инверсии управления
- Модули верхних уровней не должны зависеть от модулей нижних уровней. Модули обоих уровней должны зависеть от абстракций.
- Абстракции не должны зависеть от деталей. Детали должны зависеть от абстракций.
Показатели качества дизайна, которые устраняются с применением Инверсии управления
- Жесткость. Изменение одного модуля ведет к изменению других модулей.
- Хрупкость. Изменения в одной части приводят к неконтролируемым ошибкам в других частях программы.
- Неподвижность. Модуль сложно отделить от остальной части приложения для повторного использования.
Внедрение зависимости (DI)
Процесс предоставления внешней зависимости программному компоненту. Является специфичной формой «инверсии управления» (англ. Inversion of control, IoC), когда она применяется к управлению зависимостями. В полном соответствии с принципом единой обязанности объект отдаёт заботу о построении требуемых ему зависимостей внешнему, специально предназначенному для этого общему механизму.
Так вот Dagger 2 как раз и берет на себя заботу создания этого общего механизма.
Предчувствуя вопросы и холивары по IoC, DI, как они соотносятся друг с другом, я добавлю, что определения были взяты с Википедии, и подробное обсуждение выходит за рамки статьи.
Теперь перечислим основные преимущества библиотеки.
Преимущества Dagger 2
- Простой доступ к “расшаренным” реализациям.
- Простая настройка сложных зависимостей. Чем больше у вас приложение, тем больше становится зависимостей. Dagger 2 позволяет вам по-прежнему легко контролировать все зависимости.
- Облегчение Юнит-тестирования и интеграционного тестирования. Данный вопрос обсудим в статье, посвященной тестированию c Dagger 2.
- “Локальные” синглтоны.
- Кодогенерация. Полученный код понятен и доступен для отладки.
- Никаких проблем при обфускации. И пятый, и шестой пункты являются отличительными свойствами Dagger второй версии от первой. Dagger 1 работал на рефлексии. Отсюда проблемы с производительностью, обфускацией, загадочными падениями в рантайме.
- Малые размеры библиотеки
Для примера простого доступа к “расшаренным” реализациям приведу код:
public class MainActivity extends AppCompatActivity {
@Inject RxUtilsAbs rxUtilsAbs;
@Inject NetworkUtils networkUtils;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
App.getComponent().inject(this);
}
}
То есть к полям добавляются аннотации @Inject
и в метод onCreate
добавляется строчка App.getComponent().inject(this);
. И теперь классу MainActivity
доступны готовые реализации RxUtilsAbs
и NetworkUtils
.
Все эти вышеперечисленные преимущества делают Dagger 2 лучшей библиотекой для реализации DI в Android на данный момент.
Конечно, у библиотеки есть и недостатки. Но о них мы поговорим уже в конце цикла статей. Сейчас-то моя задача заинтересовать вас и подтолкнуть попробовать Dagger 2.
Основные элементы(аннотации) Dagger 2:
@Inject
– базовая аннотация, с помощью которой “запрашивается зависимость”@Module
– классы, чьи методы “предоставляют зависимости”@Provide
– методы внутри@Module
, “говорящие Dagger, как мы хотим сконструировать и предоставить зависимость“@Component
– мост между@Inject
и@Module
@Scope
– предоставляют возможность создания глобальных и “локальных синглтонов”@Qualifier
– если необходимы разные объекты одного типа
Пока что просто просмотрите данные аннотации для общего ознакомления. Каждую из них мы подробно обсудим.
Собственно, по теории ограничимся этим. Более подробно можно ознакомиться по ссылкам в конце статьи.
Основная же наша цель — это понять, как происходит построение всего графа зависимостей с помощью Dagger 2.
Практика
Теперь начинаются уже более интересные вещи.
Рассмотрим конкретный пример. У всех в приложениях есть синглтоны. В Android без них никуда, учитывая жизненные циклы активити и фрагментов.
При этом имеющиеся синглтоны я бы разделил на две категории:
- "Глобальные" синглтоны, которые могут понадобиться в любой части приложения. К ним относятся Context, утилитные классы и прочие классы, влияющие на работу всего приложения
- "Локальные" синглтоны, которые нужно только в определенном одном или нескольких модулях. Но из-за возможных переориентаций экрана и прочего, часто возникает необходимость выноса части логики и данных в независимое от жизненного цикла место. О "локальных" синглтонах более подробно и схематично в следующей статье.
Начнем с "глобальных" синглтонов. Как обычно мы их используем? Смею предположить, что в большинстве имеет место следующий код:
SomeSingleton.getInstance().method();
Обычная практика. Но если мы хотим применять у себя паттерн DI, то данный код будет не удовлетворителен по нескольким причинам:
- В классе, где используется подобный вызов, внезапно возникает зависимость от класса
SomeSingleton
. Это неявная зависимость, она нигде четко не обозначена (ни в конструкторе, ни в полях, ни в методах). Поэтому увидеть такую зависимость можно только просматривая код конкретного метода, а ведь по интерфейсу класса и не скажешь, что здесь применяется данныйSomeSingleton
. - Процессом инициализации занимается сам
SomeSingleton
. А если используется ленивая инициализация, то стартует процесс инициализации какой-нибудь из классов, применяющихSomeSingleton
(где первым вызовется). То есть классы, помимо своей работы, отвечают еще и за старт инициализации Синглтона. - С увеличением количества таких Синглтонов система покрывается сетью неявных зависимостей. Еще одни синглтоны могут зависеть от других, что также не упрощает дальнейшее их сопровождение. Плюс синглтоны разбросаны по системе, могут находится в разных пакетах, и это причиняет некоторые неудобства.
С этим всем, конечно же, можно жить. Нелегко, но можно. Но все начинает в корне меняться, когда вы захотите обложить свой код юнит-тестами. Вот тут вам придется что-то делать с этими неявными зависимостями, как-то совершать их корректную "подмену". Вы начинаете волей-неволей преобразовывать свой код в "тестируемый код", а с неявными зависимостями — это нереально.
А теперь о Dagger 2 (по ходу статьи я буду иногда называть ее просто по-русски — "Даггер"). Сейчас мы увидим, как с помощью Dagger 2 можно реализовать синглтоны по DI. А заодно увидим весь цикл создания графа зависимостей.
Начнем с "глобальных" синглтонов.
Создание синглтонов
Как мы помним, @Module
— это аннотация, помечающий класс, чьи методы "предоставляют зависимости" ("провайдят зависимости"). В дальнейшем подобные классы мы будем называть просто Модулями. А методы, которые "провайдят зависимости" или "предоставляют зависимости", будем называть provide-методами.
К примеру в ReceiversModule
есть метод provideNetworkChannel
, который как раз и предоставляет объект типа NetworkChannel
. Данный метод на самом деле может называться как угодно, самое главное — аннотация @Provides
перед методом и возвращаемый тип (NetworkChannel
).
Распространена практика, когда возвращаемый тип — это интерфейс или абстрактный класс (RxUtilsAbs
), а внутри метода мы уже инициализируем и возвращает нужную реализацию (RxUtils
).
Про аннотацию @Singleton
ниже, пока не обращаем внимания на нее.
Также в модуль, в конструктор, можно передавать необходимые объекты. Пример — AppModule
.
А с UtilsModule
уже интереснее. Чтобы предоставить свои зависимости — RxUtilsAbs
и NetworkUtils
ему необходимы объекты типов Context
и NetworkChannel
. Значит мы должны как-то сказать Даггеру, что при создании объектов RxUtilsAbs
и NetworkUtils
необходимы Context
и NetworkChannel
. Для этого в методы provideRxUtils
и provideNetworkUtils
добавляются аргументы: Context context
для первого и Context context, NetworkChannel networkChannel
для второго.
При этом название аргументов может быть любое, хоть context
, хоть contextSuper
, без разницы. Главное, это типы аргументов.
Далее создаем интерфейс AppComponent
с аннотацией
@Component(modules = {AppModule.class, UtilsModule.class, ReceiversModule.class})
.
Для удобства подобный интерфейс мы будем называть Компонентом.
Как говорилось выше, @Component
по сути является мостом между @Module
и @Inject
. Или другими словами, Компонент представляет собой готовый граф зависимостей. Что это означает? Поймем чуть ниже.
Данной аннотацией мы говорим Даггеру, что AppComponent
содержит три модуля — AppModule, UtilsModule, ReceiversModule
. Зависимости, которые провайдит каждый из этих модулей, доступны для всех остальных модулей, объединенных под эгидой компонента AppComponent
. Для большей наглядности взглянем на рисунок.
Я думаю, с помощью этого рисунка станет намного понятней, откуда Даггер берет объекты Context
и NetworkChannel
для построения RxUtilsAbs
и NetworkUtils
. Если из аннотации компонента убрать модуль AppModule
, например, то при компиляции Даггер заругается и спросит, откуда ему взять объект Context
.
Также внутри интерфейса объявляем метод void inject(MainActivity mainActivity)
. Этим методом мы сообщаем Даггеру, в какой класс/классы мы хотим делать инъекции.
Едем дальше. Помните, мы в AppComponent
целью инъекций задали класс MainActivity
. В данном классе мы можем использовать те зависимости, которые провайдят модули AppModule, UtilsModule, ReceiversModule
. Для этого нужно просто добавить в класс соответствующие поля и пометить их аннотацией @Inject
, а также сделать их доступность как минимум пакетной (если поле задано как private
, то Даггер не сможет подставить в это поле нужную реализацию).
Также отмечу, что в поле RxUtilsAbs rxUtilsAbs
подставляется класс RxUtils
(RxUtils
— наследник RxUtilsAbs
), то есть то, что мы и задали в модуле UtilsModule
.
Далее в методе onCreate
мы добавляем строчку
App.getComponent().inject(this);
Так как рассматриваем мы создание синглтонов, то компонент наш AppComponent
лучше хранить в классе Application
. В нашем примере получить доступ к AppComponent
можно через App.getComponent()
.
Вызывая же метод inject(MainActivity mainActivity)
, мы окончательно связываем наш граф зависимостей. Таким образом, все зависимости, которые провайдят модули AppComponent
(Context
, NetworkChannel
, RxUtilsAbs
, NetworkUtils
), становятся доступны в MainActivity
.
Обратим внимание на метод buildComponent()
класса App
. DaggerAppComponent
до компиляции нам не доступен.
Поэтому в начале не обращаем внимания на IDE, которая будет говорить, что класса DaggerAppComponent
не существует. Ну и еще IDE не будет подсказывать при построении билдера. Так что инициализацию AppComponent
с поомощью DaggerAppComponent
придется писать "вслепую" первый раз.
Dagger 2, как мы уже говорили, отвечает за создание всего графа зависимости. Если что-то пойдет не так, он вам сообщит при компиляции. Никаких неожиданных и непонятных падений в рантайме, как это было, например, с Dagger 1.
А теперь внимание на схему ниже!
Фуф, теперь можно выдохнуть! Наиболее насыщенная часть позади. Мне кажется, данная схема наглядно демонстрирует, что:
- Модуль провайдит зависимости. То есть именно в модулях мы прописываем, какие объекты хотим предоставлять.
- Компонент являет собой граф зависимостей. Он объединяет модули и предоставляет зависимости нуждающимся классам (
MainActivity
)
Если что-то не понятно или не явно, пишите в комментариях, исправим и объясним!
Ну и напоследок рассмотрим аннотацию @Singleton
. Это Scope-аннотация, предоставляемая Даггером. Если перед методом, который провайдит зависимость, поместить @Singleton
, то Даггер при инициализации Компонента, создаст единственный экземпляр помеченной зависимости, то есть синглтон. И при каждом затребовании данной зависимости будет предоставлять этот единственный экземпляр.
Меньше слов, больше картинок!
Каждая зависимость провайдится с аннотацией @Singleton
. Это значит, что каждый раз, когда Даггеру необходимо будет использовать данную зависимость, он будет использовать только ее один экземпляр.
Теперь для сравнения уберем у метода provideNetworkChannel
аннотацию @Singleton
(зависимость становится "unscoped"). Это значит, что когда Даггеру необходимо будет использовать данную зависимость, он будет каждый раз создавать новый ее экземпляр.
Мы можем также создавать кастомные Scope-аннотации (подробнее в следующей статье).
Приведем некоторые особенности Scope-аннотаций:
- Обычно scope-аннотации задаются для Компонента и provide-метода.
- Если хоть один provide-метод имеет scope-аннотацию, то Компонент должен иметь точно такую же scope-аннотацию.
- Компонент может быть "unscoped", только если во всех его модулях все provide-методы также "unscoped".
- Все scope-аннотации в рамках одного компонента (то есть для всех модулей с provide-методами, входящих в состав Компонента, и у самого Компонента) должны быть одинаковыми.
Более подробно тема со Scope-аннотациями будет раскрыта в следующей статье. А для начала нам хватит и этого :)
Итак, в данной статье мы ознакомились с теоретическими аспектами IoC, DI, Dagger 2. Рассмотрели подробно создание графа зависимостей с помощью Dagger 2, частично познакомились со scope-аннотациями и ее конкретной реализацией @Singleton
.
Привожу список статей, которые рекомендую к прочтению:
- Официальная страница библиотеки
- Презентация от Google
- Статья Fernando Cejas
- Статья от Miroslaw Stanek
- Первая часть цикла статей от Antonio Leiva
- Хорошая статья со схемками
Также отдельно отмечу русскоязычный подкаст, посвященный android-разработке apptractor, на котором в самое ближайшее время будет обсуждение Dagger 2.
Жду комментарии, отзывы и вопросы!
Автор: xoxol_89