Dagger 2

Нарешті приспіла третина циклу статей про Dagger 2!

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

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

Є, звісно, ​​моменти, які розписані більш-менш. Але всякі нові фічі описані так, що мені доводилося методом проб і помилок, залазячи в згенерований код, самому розбиратися, як воно все працює. Добре хороші люди пишуть хороші статті, але навіть іноді вони не дають чіткої та чіткої відповіді відразу.

Отже, годі говорити, і вперед до нових знань!

Qualifier annotation

Часто буває, що нам необхідно пройти кілька об'єктів одного типу. Наприклад, ми хочемо мати в системі два Executor: один однопотоковий, інший з Cached ThreadPool. У цьому випадку нам приходить на допомогу "qualifier annotation". Це кастомна інструкція, яка має анотацію @Qualifier . Звучить трохи як олія, але на прикладі все набагато простіше.

Загалом, Dagger2 надає нам уже одну готову "qualifier annotation", якої, мабуть, цілком достатньо у повсякденному житті:

А тепер подивимося, як це все виглядає у бою:

У результаті у нас два різних екземпляри (singleExecutor, multiExecutor) одного класу (Executor). Те, що нам потрібно! Зауважу, що об'єкти одного класу з анотацією @Named можуть пройти також як з абсолютно різних і незалежних компонентів, так і з залежних один від одного.

Відкладена ініціалізація

Одна знайпоширеніших наших розробницьких проблем - це довгий старт програми. Зазвичай причина в одному — ми дуже багато вантажимо і ініціалізуємо при старті. Крім того, Dagger2 будує граф залежностей переважно потоці. І часто далеко не всі об'єкти, що конструюються Даггером, потрібні відразу ж. Тому бібліотека дає можливість відкласти ініціалізацію об'єкта до першого виклику за допомогою інтерфейсів Provider<> та Lazy<> . Одразу звернемо наш погляд на приклад:

Почнемо з Provider singleExecutorProvider. Допершого виклику singleExecutorProvider.get() Даггер не ініціалізує відповідний Executor . Але прикожному наступному виклику singleExecutorProvider.get() буде створюватися новий екземпляр. Таким чином, singleExecutor і singleExecutor2 — це два різні об'єкти. Така поведінка по суті ідентична поведінці об'єкта.

Тепер Lazy multiExecutorLazy і Lazy multiExecutorLazyCopy. Dagger2 ініціалізує відповідні Executor лише запершого виклику multiExecutorLazy.get() і multiExecutorLazyCopy.get() . Далі Даггер кешує проініціалізовані значення длякожного Lazy<> і під час другого виклику multiExecutorLazy.get() і multiExecutorLazyCopy.get() видає закешовані об'єкти. Таким чином multiExecutor і multiExecutor2 посилаються однією об'єкт, а multiExecutor3 на другий об'єкт. Але, якщо ми в AppModule до методу provideMultiThreadExecutor() додамо анотацію @Singleton , то об'єкт буде кешуватися для всього дерева залежностей, і multiExecutor , multiExecutor2 , multiExecutor3 будуть посилатися наодин об'єкт. Будьте уважними.

Асинхронне завантаження

Зверніть увагу на @Singleton та інтерфейс Lazy в AppModule. Lazy якраз і гарантує, що великоваговий об'єктбуде проініціалізовано, коли ми запитаємо, а потім закешовано.

А як нам бути, якщо ми хочемо щоразу отримувати новий екземпляр цього «важкого» об'єкта? Тоді варто трохи поміняти AppModule:

Для методу provideHeavyExternalLibrary() ми прибралиscope, а в provideHeavyExternalLibraryObservable(final Provider 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, то використовуйте лише скопіювати анотації. Ще наведу посилання на питання порівняння @Singleton і @Reusable зі SO.

@Subcomponent.Builder

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

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

Створення FirstComponentтепер виглядає так:

Тепер у нас є можливість робити так:

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

А якщо додати static до currentUser:

Зверніть увагу, що у варіанті зі static немає AuthModel . Таким чином, статичний метод смикається компонентомбезпосередньо, минаючи модуль. А якщо в модулі лише одні статичні методи, то екземпляр модуля навіть не створюється. Економія та мінус зайві виклики. Власне, у нас виграш за продуктивністю. Також пишуть, що виклик статичного методу на 15–20% швидше за виклик аналогічного нестатичного методу. Якщо я помиляюся, iamironz виправить мене. Вже він точно знає, а якщо потрібно, і заміряє.

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

Мегазручна зв'язка, яка значно зменшує boilerplate-code. На зорі вивчення Даггера я не розумів, навіщо потрібні ін'єкції конструктора. Що й звідки береться. А тут ще з'явився @Binds. Але все насправді досить просто. Дякуємо за допомогуВолодимиру Тагакову і ось цій статті. Розглянемо типову ситуацію. Є інтерфейс Презентера та його реалізація:

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

Припустимо, що наш FirstPresenter потребує класів-помічників, яким він делегує частину роботи. Для цього необхідно в модулі створити ще два методи, які будуть провайди нові класи, потім змінити конструктор FirstPresenter, а відтак і оновити відповідний метод у модулі. Модуль буде такий:

І так от щоразу, якщо потрібно додати якийсь клас і «розшарити»його іншим. Модуль забруднюється дуже швидко. І якось надто багато коду, не знаходите? Але є рішення, яке суттєво зменшує код. По-перше, якщо нам необхідно створити залежність і віддавати готовий клас, а не інтерфейс (HelperClass1 і HelperClass2), ми можемо вдатися до ін'єкції конструктора. Виглядати це буде так:

Зверніть увагу, що до класів було додано інструкцію @FirstScope , таким чином Даггер розуміє, до якого дерева залежностей віднести ці класи. Тепер з модуля ми можемо сміливо прибирати провайдинг HelperClass1 та HelperClass2 :

Як можна ще зменшити код у модулі? Ось тут застосуємо @Binds:

А в FirstPresenter зробимо ін'єкцію конструктора:

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

Про що ще не сказано

Releasable references. Якщо з пам'яттю зовсім біда. За допомогою відповідних анотацій ми помічаємо об'єкти, якими можемо пожертвувати за нестачі пам'яті. Ось такий ось хак. У доках (підрозділ Releasable references) цілком зрозуміло описано, хоч як дивно.

Тестування. Звичайно, для Unit-тестування Даггер не потрібен. А ось для функціональних, інтеграційних та UI тестів може знадобитися можливість заміни певних модулів. Дуже добре цю тему розкриває Artem_zin у своїй статтіта прикладі. У документації виділено розділ із питання тестування. Але знову ж таки гуглівці не можуть нормально описати, як саме підмінити компонент. Як правильно створити фейкові модулі та підставити їх. Для заміни компонента (окремих модулів) я користуюся методом Артема. Так, хотілося б, щоб можна було створити окремим класом тестовий компонент та окремими класами тестові модулі, і красиво все це підключити до тестового Application файлу. Може, хто знає?

@BindsOptionalOf. Працює разом з Optional від Java 8 або Guava, що робить цю фічу вже важкодоступною для нас. Якщо цікаво, наприкінці документації можна знайти опис.

  • @BindsInstance. На жаль, у dagger 2.8 мені ця фіча виявилася недоступною. Основне посилання її в тому, що вистачить передавати будь-які об'єкти через конструктор модуля. Дуже поширений приклад, коли через конструктор AppComponent передається глобальний Context. Так ось із цією інструкцією такого робити стане не потрібно. Наприкінці документації є приклад.
  • Ну от і все! Начебто всі моменти вдалося висвітлити. Якщо щось пропустив чи недостатньо описав, пишіть! Виправимо. Також рекомендую групу по Dagger2 у Телеграмі, де ваші запитання не залишаться без відповідей. Крім того, правильне застосування бібліотеки пов'язане з чистою архітектурою. Тому ось вам і гурт з архітектури. І так, незабаром на AndroidDevPodcast планується випуск, присвячений Даггер. Слідкуйте за новинами!