Dagger 2. Часть третья. Новые грани возможного

Dagger 2. Часть третья. Новые грани возможного

Всем привет! Наконец-то подоспела третья часть цикла статей о Dagger 2!

Перед дальнейшим прочтением настоятельно рекомендую ознакомиться с первой и второй частями.

Большое спасибо за отзывы и комментарии. Я очень рад, что мои статьи действительно помогают разработчикам окунуться в мир Даггера. Именно это и придает силы творить для вас дальше. В третьей части мы с вами рассмотрим различные интересные и немаловажные фичи библиотеки, которые могут вам очень пригодиться.

Вообще библиотека существует уже приличное время, но документация по-прежнему крайне отвратная. Разработчику, который только начинает свое знакомство с Даггером, я бы даже посоветовал не заглядывать в официальную документацию вначале, дабы не разочаровываться в этом жестком и несправедливом мире.

Есть, конечно, моменты, которые расписаны более-менее. Но вот всякие новые фичи описаны так, что мне приходилось методом проб и ошибок, залезая в сгенерированный код, самому разбираться, как оно все работает. Благо хорошие люди пишут хорошие статьи, но даже иногда они не дают четкого и ясного ответа сразу.

Итак, хватит разглагольствовать, и вперед к новым знаниям!

Qualifier annotation

В прошлой статье в комментариях попросили осветить данный вопрос. Не будем откладывать в долгий ящик.

Часто бывает, что нам необходимо провайдить несколько объектов одного типа. Например, мы хотим иметь в системе два Executor : один однопоточный, другой с CachedThreadPool . В этом случае нам приходит на помощь "qualifier annotation". Это кастомная аннотация, которая имеет в себе аннотацию @Qualifier . Звучит немного как масло масляное, но на примере все гораздо проще.

В общем, Dagger2 предоставляет нам уже одну готовую "qualifier annotation", которой, пожалуй, вполне достаточно в повседневной жизни:

А теперь посмотрим, как это все выглядит в бою:

В итоге у нас два разных экземпляра ( singleExecutor , multiExecutor ) одного класса ( Executor ). То, что нам и нужно! Замечу, что объекты одного класса с аннотацией @Named могут провайдиться также как с абсолютно разных и независимых компонентов, так и c зависимых друг от друга.

Отложенная инициализация

Одна из распространенных наших разработческих проблем — это долгий старт приложения. Обычно причина в одном — мы слишком много всего грузим и инициализируем при старте. Кроме того, Dagger2 строит граф зависимостей в основном потоке. И часто далеко не все конструируемые Даггером объекты нужны сразу же. Поэтому библиотека дает нам возможность отложить инициализацию объекта до первого вызова с помощью интерфейсов Provider<> и Lazy<> .

Сразу же обратим наш взор на пример:

Начнем с Provider<Executor> singleExecutorProvider . До первого вызова singleExecutorProvider.get() Даггер не инициализирует соответствующий Executor . Но при каждом последующем вызове singleExecutorProvider.get() будет создаваться новый экземпляр. Таким образом singleExecutor и singleExecutor2 — это два разных объекта. Такое поведение по сути идентично поведению unscoped объекта.

В каких вообще ситуациях уместен Provider ? Он пригождается, когда мы провайдим какую-то мутабельную зависимость, меняющую свое состояние в течении времени, и при каждом обращении нам необходимо получать актуальное состояние. "Что за кривая архитектура?" — скажите вы, и я с вами соглашусь. Но при работе с legacy кодом и не такое увидишь.

Отмечу, что авторы библиотеки тоже не советуют злоупотреблять интерфейсом Provider в тех местах, где достаточно обойтись обычным unscope, так как это чревато "кривой архитектурой", как говорилось выше, и трудно отлавливаемыми багами.

Теперь Lazy<Executor> multiExecutorLazy и Lazy<Executor> multiExecutorLazyCopy . Dagger2 инициализирует соответствующие Executor только при первом вызове multiExecutorLazy.get() и multiExecutorLazyCopy.get() . Далее Даггер кэширует проинициализированные значения для каждого Lazy<> и при втором вызове multiExecutorLazy.get() и multiExecutorLazyCopy.get() выдает закэшированные объекты.

Таким образом multiExecutor и multiExecutor2 ссылаются на один объект, а multiExecutor3 на второй объект.

Но, если мы в AppModule к методу provideMultiThreadExecutor() добавим аннотацию @Singleton , то объект будет кешироваться для всего дерева зависимостей, и multiExecutor , multiExecutor2 , multiExecutor3 будут ссылаться на один объект.

Асинхронная загрузка

Мы подошли с вами к весьма нетривиальной задаче. А что, если мы хотим, чтобы конструирование графа зависимостей проходило в бэкграунде? Звучит многообещающе? Да-да, я про Producers.

Честно скажу, это тема заслуживает вообще отдельного рассмотрения. Там много особенностей и нюансов. По ней достаточно хорошего материала. Сейчас же я коснусь только плюсов и минусов Producers.

Плюсы. Ну самый главный плюс — это загрузка в бэкграунде и возможность управлять этим процессом загрузки.

Минусы. Producers "тащат" за собой Guava, а это плюс 15 тысяч методов к апк. Но самое плохое, что применение Producers немного "портят" общую архитектуру и делают код более запутанным. Если у вас уже был Даггер, а потом вы решили перенести инициализацию объектов в бэкграунд, вам придется хорошенько постараться.

В официальной документации данная темы выделена в специальный раздел. Но я очень рекомендую статьи Miroslaw Stanek. У него вообще очень хороший блог, и там много статей про Dagger2. Собственно, некоторые даже макеты картинок с прошлых статей я заимствовал у него. Про Producers он пишет в этой статье.

А вот в следующей предлагает очень интересную альтернативу для загрузки дерева зависимостей в бэкграунде. На помощь приходит родная RxJava. Мне очень нравится его решение, так как оно полностью лишено недостатков использования Producers, но при этом решает вопрос асинхронной загрузки.

Один только минус: Мирослав не совсем верно применяет Observable.create(. ) . Но я об этом написал в комментарии к статье, так что обратите внимание обязательно.

А теперь посмотрим, как будет выглядеть тогда код для scope объекта (с "правильной" RxJava):

Обратите внимание на @Singleton и интерфейс Lazy в AppModule . Lazy как раз и гарантирует, что тяжеловесный объект будет проинициализирован, когда мы запросим, а затем закеширован.

А как нам быть, если мы хотим каждый раз получать новый экземпляр этого "тяжелого" объекта? Тогда стоит немного поменять AppModule :

Для метода provideHeavyExternalLibrary() мы убрали scope, а в provideHeavyExternalLibraryObservable(final Provider<HeavyExternalLibrary> heavyExternalLibraryLazy) используем Provider вместо Lazy . Таким образом heavyExternalLibrary и heavyExternalLibraryCopy в MainActivity — это разные объекты.

А можно еще вообще весь процесс инициализации дерева зависимостей вынести в бэкграунд. Вы спросите, как? Очень даже легко. Сначала посмотрим на то, как было:

А теперь взглянем на обновленный метод void setupActivityComponent() (с моими правками по RxJava):

Замеры

В прошлом разделе мы говорили про производительность при старте приложения. Однако мы знаем, что, если вопрос касается производительности и скорости, мы должны замерять! Полагаться на интуицию и чувство "вроде бы стало быстрее" нельзя. И с этим нам снова поможет Мирослав в этой и этой статьях. Чтобы мы все делали без него, вообще не представляю.

Новые интересные возможности

У Даггера появляются новые интересные фичи, обещающие нам облегчить жизнь. Но вот понять, как все работает и что же нам это все дает, — было задачей не из легких. Ну что же, начнем!

@Reusable scope

Интересная аннотация. Позволяет экономить память, но при этом по сути не ограничена никаким scope , что делает очень удобным переиспользование зависимостей в любых компонентах. То есть это нечто среднее между scope и unscope .

В доках пишут очень важный момент, который как-то не бросается в глаза с первого раза: "Для каждого компонента, который использует @Reusable зависимость, данная зависимость кешируется отдельно". И мое дополнение: "В отличии от scope аннотации, где объект кешируется при создании и его экземпляр используется дочерними и зависимыми компонентами".

А теперь сразу пример, чтобы все понять:

Наш главный компонент.

У AppComponent есть два Subcomponent . Обратили внимание на эту конструкцию — FirstComponent.Builder ? О ней мы чуть позже. Теперь посмотрим на UtilsModule .

NumberUtils с аннотацией @Reusable , а StringUtils оставим unscoped . Далее у нас два Subcomponents .

Как мы видим, FirstComponent инжектирует только в MainActivity , а SecondComponent — в SecondActivity и ThirdActivity . Посмотрим код.

Коротко про навигацию. Из MainActivity мы попадаем в SecondActivity , а затем в ThirdActivity . А теперь вопрос. Когда мы будем уже на третьем экране, сколько объектов NumberUtils и StringUtils будет создано?

Так как StringUtils — unscoped , то будет создано три экземпляра, то есть при каждой инъекции создается новый объект. Это мы знаем.

А вот объектов NumberUtils будет два — один для FirstComponent , а другой для SecondComponent . И здесь я снова приведу основную мысль про @Reusable с документации: "Для каждого компонента, который использует @Reusable зависимость, данная зависимость кешируется отдельно!", в отличии от scope аннотации, где объект кешируется при создании и его экземпляр используется дочерними и зависимыми компонентами.

Но сами гугловцы предупреждают, что если вам необходим уникальный объект, который может быть еще и mutable, то используйте только scoped аннотации.

Еще приведу ссылку на вопрос про сравнение @Singleton и @Reusable со SO.

@Subcomponent.Builder

Фича, которая делает код красивее. Раньше, чтобы создать @Subcomponent нам приходилось писать нечто такое:

Мне не нравилось в этом подходе то, что родительский компонент был загружен ненужными знаниями о модулях, которые используют дочерние сабкомпоненты. Ну и плюс передача большого количества аргументов выглядит не очень красиво, ведь для этого есть паттерн Builder. Теперь стало красивее:

Создание FirstComponent теперь выглядит следующим образом:

static

Теперь у нас есть возможность делать вот так:

То есть методы, отвечающие за провайдинг зависимостей в модулях, мы можем делать статическими. Я сначала не совсем понимал, а зачем это вообще нужно то? А оказывается, запрос на такую фичу существовал довольно давно, и есть ситуации, когда это выгодно.

На SO задали хороший вопрос на эту тему, мол, а чем собственно отличаются @Singleton от @Provide static . Чтобы хорошо понять эту разницу, нужно читать ответ на вопрос, параллельно экспериментируя и смотря сгенерированный код.

Итак, у нас есть вводная. Мы имеем три варианта одного и того же метода в модуле:

При этом authManager.currentUser() в разные моменты времени может отдавать разные экземпляры. Логичный вопрос: а чем эти методы отличаются.

В первом случае у нас классический unscope . При каждом запросе будет отдаваться новый экземпляр authManager.currentUser() (точнее новая ссылка на currentUser ).

Во втором случае при первом запросе будет закеширована ссылка на currentUser , и при каждом новом запросе будет отдаваться эта ссылка. То есть, если поменялся currentUser в AuthManager , то отдаваться то будет старая ссылка на невалидный уже экземпляр.

Третий случай уже интереснее. Данный метод по поведению аналогичен unscope , то есть при каждом запросе будет отдаваться новая ссылка. Это первое отличие от @Singleton , который кеширует объекты. Таким образом размещать в @Provide static методе инициализацию объекта не совсем уместно.

Но в чем тогда @Provide static отличается от unscope ? Допустим у нас есть такой модуль:

AuthManager поставляется из другого модуля в качестве Singleton . Теперь быстро окинем взглядом сгенерированный код AuthModule_CurrentUserFactory (в студии просто поставьте курсор на currentUser и нажмите Ctrl+B):

А если добавить static к currentUser :

Обратите внимание, что в варианте со static нет AuthModule . Таким образом, статический метод дергается компонентом напрямую, минуя модуль. А если в модуле только одни статические методы, то экземпляр модуля даже не создается.

Экономия и минус лишние вызовы. Собственно у нас выигрыш по производительности. Также пишут, что вызов статического метода на 15-20% быстрее вызова аналогичного нестатического метода. Если я ошибаюсь, iamironz поправит меня. Уж он то точно знает, а если нужно, и замерит.

@Binds + Inject конструктора

Мегаудобная связка, которая значительно уменьшает boilerplate-code. На заре изучения Даггера я не понимал, зачем нужны инъекции конструктора. Что и откуда берется. А тут еще появился @Binds. Но все на самом деле довольно просто. Спасибо за помощь Владимиру Тагакову и вот этой статье.

Рассмотрим типичную ситуацию. Есть интерфейс Презентера и его реализация:

Мы, как белые люди, провайдим все это дело в модуле и инжектим интерфейс Презентера в активити:

Допустим, что наш FirstPresenter нуждается в классах-помощниках, которым он делегирует часть работы. Для этого необходимо в модуле создать еще два метода, которые будут провайдить новые классы, потом изменить конструктор FirstPresenter , а следственно и обновить соответствующий метод в модуле.

Модуль будет такой:

И так вот каждый раз, если нужно добавить какой-то класс и "расшарить" его другим. Модуль "загрязняется" очень быстро. И как-то слишком много кода, не находите? Но есть решение, которое существенно уменьшает код.

Во-первых, если нам необходимо создать зависимость и отдавать готовый класс, а не интерфейс ( HelperClass1 и HelperClass2 ), мы можем прибегнуть к инъекции конструктора. Выглядеть это будет следующим образом:

Обратите внимание, что к классам была добавлена аннотация @FirstScope , таким образом Даггер понимает, в какое дерево зависимостей отнести данные классы.

Теперь с модуля мы можем смело убирать провайдинг HelperClass1 и HelperClass2 :

Как можно еще уменьшить код в модуле? Вот здесь применим @Binds :

А в FirstPresenter сделаем инъекцию конструктора:

Какие здесь новшества? FirstModule стал у нас абстрактным, как и метод provideFirstPresenter . У provideFirstPresenter убрали аннотацию @Provide , зато добавили @Binds . А в аргументы передаем не необходимые зависимости, а конкретную реализацию! У FirstPresenter добавилась scope аннотация — @FirstScope , по которой Даггер понимает, куда отнести данный класс. Также к конструктору добавили аннотацию @Inject . Стало намного чище, и добавлять новые зависимости стало еще проще!

Пару ценных дополнений по абстрактным модулям от Mujahit. Давайте вспомним, что FirstModule относится к FirstComponent , который в свою очередь является сабкомпонентом от AppComponent . И чтобы создать FirstComponent мы делали вот так:

Но как нам создать то экземпляр FirstModule , если он является абстрактным? В прошлых статья я упоминал, что если мы в конструктор модулей ничего не передаем, то есть используем конструкторы по умолчанию, то при создании компонента инициализацию этих модулей можно опустить:

А у себя внутри Даггер уже сам разруливает, что делать с абстрактными и неабстрактными модулями и как провайдить все необходимые зависимости.

Также замечу, что если у модуля одни абстрактные методы, то модуль можно реализовать через интерфейс:

Кроме того в абстрактный модуль мы можем также добавить только статические методы. "Обычные" методы добавить не можем:

О чем еще не сказано

Далее я приведу еще список фич с коротким описанием и ссылками на качественное объяснение:

Muitibindings. Позволяет "байндить" объекты в коллекции ( Set и Map ). Подходит для реализации архитектуры расширения ("plugin architecture"). Крайне рекомендую вот это очень подробное описание с азов. Более интересные примеры применения Muitibindings можно найти в статьях Мирослава тут и тут. И еще в придачу ссылка на официальную документацию. Так что мне даже нечего добавить по данному вопросу.

Releasable references. Если уж с памятью совсем беда. С помощью соответствующих аннотаций мы помечаем объекты, которыми можем пожертвовать при недостатке памяти. Вот такой вот хак. В доках (подраздел Releasable references) вполне все понятно описано, как ни странно.

Тестирование. Конечно же, для Unit-тестирования Даггер не нужен. А вот для функциональных, интеграционных и UI тестов может пригодиться возможность подмены определенных модулей. Очень здорово эту тему раскрывает Artem_zin в своей статье и примере. В документации выделен раздел по вопросу тестирования. Но опять-таки гугловцы не могут нормально описать, как именно подменить компонент. Как правильно создать фэйковые модули и подставить их. Для подмены компонента (отдельных модулей) я пользуюсь способом Артема. Да, хотелось бы, чтобы можно было создать отдельным классом тестовый компонент и отдельными классами тестовые модули, и красиво все это подключить в тестовом Application файле. Может кто знает?

@BindsOptionalOf. Работает вместе с Optional от Java 8 или Guava, что делает данную фичу уже труднодоступной для нас. Если интересно, в конце документации можно найти описание.

Ну вот и все! Вроде все моменты удалось осветить. Если что-то пропустил или недостаточно описал, пишите! Исправим. Также рекомендую группу по Dagger2 в Телеграме, где ваши вопросы не останутся без ответов.

Кроме того, правильное применение библиотеки очень связано с чистой архитектурой. Поэтому вот вам и группа по архитектуре. И да, скоро на AndroidDevPodcast планируется выпуск, посвященный Даггеру. Следите за новостями!

📎📎📎📎📎📎📎📎📎📎