Як Anvil зменшує кількість boilerplate-коду Android-розробникам
Всім привіт! Мене звати Володимир, я Android Engineer в компанії Uptech. Уже декілька років я створюю Android-застосунки, що оптимізують бізнес-процеси та роблять життя людей трошки простішим.
У цій статті я хочу поділитись досвідом покращення Dependency Injection у проєкті.
Dependency Injection (DI) — невід’ємна частина сучасних проєктів. Цей патерн допомагає скорочувати шаблонне створення ручної ін’єкції залежностей у вашому проєкті. Коли я прийшов на проєкт, DI було реалізовано за допомогою бібліотеки Dagger 2. На момент створення цього застосунку найкраща бібліотека, яка підтримувала мультимодульність, була Dagger 2.
Тоді не існувало Hilt або ж інших зручних засобів для скорочування шаблонної ручної ін’єкції.
За деякий час роботи над проєктом наша команда вирішила знайти спрощену реалізацію DI, яка б могла добре працювати в мультимодульному середовищі й спрощувала б синтаксис зі створенням модулів. Отже потрібно було знайти рішення, яке б мало легкий перехід з Dagger 2 до іншої бібліотеки DI.
Нам дуже сподобався Anvil як доповнення до наявного Dagger 2. Він дозволяє зменшити кількість шаблонного коду й гарно працює в мультимодульному середовищі.
Тріада DI у світі Android-розробки: що і коли використовувати
Перш за все, DI — це паттерн розробки, що може бути реалізований багатьма способами, зокрема цими трьома бібліотеками, що зараз найпопулярніші для розробки Android-застосунків:
- Koin;
- Dagger 2;
- Hilt.
Koin
Koin — це гарна бібліотека для невеликих рішень. Наприклад, тестування проєкту або pet-проєкту. Це ідеальний варіант, коли ти хочеш зосередитися на створенні проєкту, написанні нового функціоналу, але не хочеш витрачати багато часу на шаблонний код. З головних особливостей цієї реалізації бібліотеки — Runtime генерація залежностей. Це пришвидшує загальний час збірки проєкту, але нижче зазначено і недолік цього підходу.
Плюси:
● швидша збірка проєкту порівняно з Dagger 2 та Hilt (за рахунок Runtime генерації графу залежностей);
● легке налаштування для тестування.
Мінуси:
● Runtime генерація залежностей;
- усі залежності, потрібні для певного екрану, будуть ініціалізуватись під час його використання, це може призвести до runtime-помилок. Тому якщо ми щось зіпсуємо у графі залежностей, проєкт однаково буде скомпільовано, але він може аварійно завершити роботу на початку або пізніше на певному екрані;
- розробник мав би перевірити, чи залежності в цій конкретній частині програми працюють правильно і чи програма не аварійно завершує роботу. У цьому разі використання Dagger/Hilt набагато безпечніше.
Dagger 2
Щодо Dagger 2 — це рішення, що гарно підходить для мультимодульних проєктів, але має надто складний синтаксис для початківців у DI.
Плюси:
● Compile time генерація залежностей;
- помилки можуть бути виявлені під час компіляції програми. Це означало б, що вам не потрібно тестувати застосунок, щоб виявити неправильно налаштований компонент. Після створення графа залежностей його можна перетворити на статичний код для створення і впровадження компонента.
● Гарно працює з мультимодульним проєктом;
- Варто зазначити, що наша команда хотіла знайти найкращий варіант
DI-бібліотеки, яка підтримує мультимодульність без складного налаштування залежностей.
Мінуси:
● Складніший синтаксис, аніж Koin та Hilt.
Hilt
Hilt створено на основі популярної бібліотеки DI Dagger, щоб отримати переваги від продуктивності під час виконання, масштабованості та підтримки Android Studio, яку надає Dagger. Він набагато простіший за синтаксисом, ніж Dagger 2, але дуже залежить від загальних Android-компонентів.
Плюси:
● Compile time генерація залежностей;
● легший синтаксис за Dagger 2.
Мінуси:
● дуже залежить від Android-компонентів (Fragments, Viewmodels);
- варто зазначити, що деякі наші модулі будуть перевикористовуватись для більш загальних модулів застосунку для різних платформ.
Anvil
Anvil — це взагалі доповнення до Dagger 2, тож у нього також compile time генерація залежностей і він також гарно працює з мультимодульними проєктами. Але його головною особливістю є спрощений синтаксис і кодогенерація.
Плюси:
● Compile time генерація залежностей;
● спрощений синтаксис порівнянно з Dagger 2.
Мінуси:
● використовує неявні залежності;
- Для Dagger 2 треба створити файли component та module, прописати там потрібні залежності й зробити зручне створення об’єкта, який потім ініціалізувати за допомогою Fragment / Activity. Це призведе до boilerplate-коду, проте в цьому випадку ми точно розуміємо, що було зроблено. Для Anvil більшість із цього буде згенеровано під капотом, що може призвести до незрозумілостей для інженерів, які мало працювали з Dagger 2.
Що таке Anvil і чим він крутіший за інші бібліотеки DI
Anvil — це доповнення до наявного Dagger 2, тож має всі особливості синтаксису останнього. У цьому розділі розповідаю про приклад використання Anvil у тестовому проєкті.
Коли ми будуємо застосунок, потрібно перш за все правильно структурувати логіку розробки на декілька кроків. Слід розуміти, що кожна нова фіча буде окремим модулем.
У Dagger 2 у кожному такому модулі потрібно створювати окремий @Scope і він буде генерувати та віддавати залежності тільки для тих класів, що мають зазначений @Scope. У нашому випадку ми використовували структуру, коли є головний AppComponent, який буде генеруватися на основі використаного AppScope, який знатиме про всі залежності компонентів та модулів.
Поглиблену інформацію щодо будування мультимодульних застосунків для Dagger 2 ви можете знайти нижче в розділі «Документація».
Головною особливістю Anvil у нашому проєкті є те, що ми маємо один великий AppComponent. Усі компоненти та модулі ми додаємо до AppScope.
Приклад створення AppScope, що буде використовуватися у всьому застосунку:
abstract class AppScope private constructor()
Швидкість у використанні одномодульного застосунку
Anvil має невелику перевагу у швидкості компілювання проєкту (одномодульних застосунків).
Таблиця з прикладом використання різних DI для одномодульного проєкту
Anvil |
9c 543мс |
9с 703мс |
9с 807мс |
9с 215мс |
9c 600мс |
Dagger 2 |
12с 109мс |
10с 194мс |
9с 758мс |
10с 451мс |
10c 500мс |
Hilt |
12с 227мс |
11с 284мс |
12с 465мс |
13с 414мс |
12с 300мс |
Таблиця з середніми результатами
Anvil |
Dagger 2 |
Hilt |
9c 573мс |
10с 602мс |
12с 338мс |
Слід зауважити, що така швидкість працюватиме лише для одномодульних проєктів. Anvil виконав збірку швидше на 10% у порівнянні з Dagger 2 і на 29% швидше за Hilt для однакового за розміром застосунку.
І невеликий опис проєкту: застосунок для показу погоди з двома екранами — одномодульний. Головна сторінка з пошуком країни та міста, і другий екран — детальна інформація погоди для обраного місця.
Таблиця з прикладом використання різних DI для мультимодульного проєкту
Anvil |
30c 243мс |
31с 503мс |
31с 807мс |
Dagger 2 |
31с 109мс |
31с 564мс |
30с 828мс |
Hilt |
32с 227мс |
33с 284мс |
33с 465мс |
Таблиця з середніми результатами
Anvil |
Dagger 2 |
Hilt |
31c 184мс |
31с 167мс |
32м 958мс |
Цей застосунок для показу погоди з трьома екранами — мультимодульний. Головна сторінка з пошуком країни та міста, і другий екран — детальна інформація погоди для обраного місця. Третій екран для налаштувань користувача.
Щодо мультимодульних проєктів, то швидкість там буде майже така сама або Anvil навіть трошки поступатиметься за швидкістю збірки проєкту. Потрібно памʼятати, що всі проєкти різні, і можуть бути випадки, коли кількість модулів, стрічок коду, певні залежності можуть змінювати загальну швидкість компілювання застосунку.
Можливості бібліотеки
Потрібно памʼятати, що Anvil є доповненням до наявної бібліотеки Dagger 2, проте Anvil має декілька дуже зручних анотацій, що спрощують повсякденну взаємодію з шаблонним кодом.
@ContributesTo використовується зазвичай для звʼязки інтерфейсів і модулів
Наприклад, працюючи з Dagger 2, ми маємо зробити інтерфейс, імплементацію і лише після цього зробити модуль, який усе це з’єднає:
interface Authenticator class RealAuthenticator @Inject constructor(): Authenticator {} @Module class AuthenticatorModule(){ @Provides fun provideAuthenticator(): Authenticator = RealAuthenticator() }
А ось приклад того, як це може бути реалізовано за допомогою Anvil:
interface Authenticator @ContributesTo(ApplicationScope::class) class RealAuthenticator @Inject constructor(): Authenticator{}
Нам не потрібно взагалі створювати модуль. Усе це робиться за допомогою анотації @ContributesTo
Коли ви працюватимете з Anvil, вам не потрібно буде створювати модулі для зʼєднування інтерфейсів і імплементацій, модуль анотація + клас до неї потрібен лише для створення ручних ініціалізацій проєкта, наприклад Retrofit, OkHttp.
@Module class BasicModule { @Singleton @Provides fun provideOkHttpClient(Logging: HttpLoggingInterceptor): OkHttpClient { return OkHttpClient.Builder() .addInterceptor (logging) .build() }
@MergeComponents
використовується замість анотації @Component
. Anvil генерує код на основі всіх необхідних модулів і компонентів, що належать одному @Scope
.
Так само ми можемо зробити з анотацією @MergeSubcomponent
, але вже для субкомпонентів. Насамперед це потрібно, коли ми маємо створити новий компонент і зʼєднати необхідні залежності з іншими наявними компонентами. І вищезазначена анотація це робить сама за розробника, тобто використовуючи один AppComponent
, воно створить необхідний клас.
@MergeComponent
@Component interface ApiComponent @Component @MergeComponent(ApiComponent::class) interface AppComponent
@MergeSubcomponent
@Component interface ApiComponent @Component @MergeComponent(ApiComponent::class) interface AppComponent
@ContributesBinding
потрібен для того, щоб додавати залежності @Binds
. Він зменшує кодогенерацію у випадках, коли у нас є проста залежність імплементації від інтерфейсу. Поглиблену інформацію ви можете знайти в розділі «Документація».
А загалом я створив невелику cheat-sheet, щоб піддивлятися перший час, що куди потрібно додавати:
І ще одна цікавинка. Anvil дозволяє підмінювати реалізації інтерфейсів на тестовi. Це надзвичайно корисно, коли нам потрібно покрити нашу логіку тестами.
@Module @ContributesTo( scope = AppScope::class, replaces = [DevelopmentApplicationModule::class] ) object DevelopmentApplicationTestModule { @Provides fun provideEndpointSelector(): EndpointSelector = TestingEndpointSelector }
Посилання (приклади)
Також ниже за посиланнями ви можете подивитися декілька прикладів з Anvil
Документація
А також кілька гарних посилань, де можна подивитися документацію
- Dagger + Anvil: Learning to Love Dependency Injection on Android | by Gabriel Peal | Medium
- square/anvil: A Kotlin compiler plugin to make dependency injection with Dagger 2 easier.
- Introducing Anvil | Square Corner Blog
- N26 Path to Anvil — DEV Community
- How to build multi-module app using Dagger 2
Висновок
Варто памʼятати, що Anvil — це не срібна куля і не вирішує всі проблеми, з якими щодня стикаються Android-розробники, але він точно робить наше життя простішим.
Використовуючи Anvil, не потрібно створювати модулі для приєднання інтерфейсів та імплементацій. Він не вносить чогось кардинально нового, проте спрощує роботу з наявним функціоналом Dagger 2.
Anvil підійде для будь-якого проєкту, де розробники хочуть покращити і зменшити кількість boilerplate коду.
Для яких проєктів НЕ підійде:
- він може були надлишковиме для проєктів, які мають дуже мало коду, краще в такому разі використовувати Koin або ж просту імплементацію Dagger 2;
- для тих, де вже використовується Hilt як основна DI на проєкті.
7 коментарів
Додати коментар Підписатись на коментаріВідписатись від коментарів