Anvil Utils: Assisted injection в многомодульных Android проектах без боли

мая 1, 2024·
Илья Гуля
Илья Гуля
· 3 мин. для прочтения

Привет, Android разработчики!

Хотел бы рассказать вам про мою небольшую библиотеку (Anvil Utils) - кодогенератор позволяющий слегка уменьшить количество боилерплейта в многомодульных проектах использующих Dagger 2 и Anvil.

В чём собственно проблема?

Представьте: у вас есть многомодульное приложение. Почти наверняка у вас есть набор фичей, каждая из которых состоит из api и implementation модулей. Представим, что один из модулей предоставляет фабрику какой-то сущности (например, Decompose компонента). Если у вас в проекте используется Dagger 2, то реализацию этой фабрики логичнее всего отдать ему, используя Assisted Injection. Однако, вам всё равно необходимо каким-то образом предоставить биндинг этой Assisted Factory к вашему публичному интерфейсу из модуля api.

Попробуем посмотреть на варианты как это можно сделать.

Голый Dagger 2

Предположим, у вас есть подобный публичный API вашего feature модуля.

interface MyClass {
	interface Factory {
	    fun create(param1: String, param2: Int): MyClass
	}
}

Обычно чтобы реализовать подобный API и сделать его доступным в вашем графе зависимостей, нужно:

  1. Создаём @AssistedFactory в implementation модуле, наследуем её от публичного интерфейса фабрики MyClass.Factory
  2. Пишем Dagger модуль который занимается биндингом сгенерированной Dagger 2 фабрики к публичному интерфейсу фабрики из модуля api.

Вот пример:

class DefaultMyClass @AssistedInject constructor(
    @Assisted param1: String,
    @Assisted param2: Int
) : MyClass {
	@AssistedFactory
	interface Factory: MyClass.Factory {
		override fun create(param1: String, param2: Int): DefaultMyClass
	}
}

@Module
interface MyClassFactoryBindingModule {
	@Binds
	fun bindFactory(impl: DefaultMyClass.Factory): MyClass.Factory
}

Также, вам придётся поддерживать руками набор параметров во всех трёх сущностях:

  • Параметры конструктора
  • Аргументы публичного интерфейса фабрики
  • Аргументы assisted фабрики Dagger

Достаточно много боилерплейта для такой простой задачи, не так ли?

Давайте упростим!

Используем Anvil

Anvil это плагин компилятора Kotlin который помогает значительно уменьшить количество боилерплейта необходимого для использования Dagger 2 в вашем приложении. Также, будучи правильно сконфигурированным, позволяет увеличить скорость сборки проекта путём избавления от необходимости запуска процессора аннотаций Dagger 2 в ваших фича-модулях. Я не буду погружаться в подробности того что такое Anvil и почему вам стоит подумать о том чтоб использовать его в вашем проекте. Возможно, напишу о нём статью попозже. Пока что, если интересует, можете прочитать документацию: https://github.com/square/anvil

Благодаря Anvil, мы можем избавиться от нашего MyClassFactoryBindingModule и использовать вместо него аннотацию @ContributesBinding:

class DefaultMyClass @AssistedInject constructor(
    @Assisted param1: String,
    @Assisted param2: Int
) : MyClass {
	@ContributesBinding(AppGraph::class, MyClass.Factory::class)
	@AssistedFactory
	interface Factory: MyClass.Factory {
		override fun create(param1: String, param2: Int): DefaultMyClass
	}
}

Anvil сгенерирует для нас соответствующий модуль который создаст биндинг DefaultMyClass.Factory к MyClass.Factory. Этот подход уменьшает количество боилерплейта, но он всё ещё тут! Нам необходимо вручную следить за соответствием публичного интерфейса фабрики, интерфейса Assisted фабрики, и параметров конструктора. Может есть вариант как упростить это ещё сильнее?

Встречайте @ContributesAssistedFactory !

Благодаря моей библиотеке Anvil Utils, вы можете попрощаться с необходимостью вручную создавать и поддерживать Assisted фабрики. Аннотация @ContributesAssistedFactory всё сделает за вас!

Она автоматически сгенерирует assisted фабрику и забиндит её в соответствующий скоуп при помощи @ContributesBinding.

Теперь мы можем полностью удалить интерфейс assisted фабрики и аннотировать наш DefaultMyClass аннотацией @ContributesAssistedFactory:

@ContributesAssistedFactory(AppGraph::class, MyClass.Factory::class)
class DefaultMyClass @AssistedInject constructor(
    @Assisted param1: String,
    @Assisted param2: Int
) : MyClass

И всё! Anvil Utils сгенерирует всё что нужно сама.

Пример из реального проекта

Для того чтобы продемонстрировать что польза не иллюзорна, я создал пулл реквест в прекрасный open source проект - приложение-компаньон для Flipper - тамагочи для хакеров. Можете ознакомиться с ним по этой ссылке, если интересно: https://github.com/flipperdevices/Flipper-Android-App/pull/798

В заключение

Если вы используете в своём проекте Anvil и у вас есть идеи что ещё можно упростить при помощи кодогенерации - открывайте пулл реквесты, возможно я реализую 🙂

Илья Гуля
Authors
Staff Software Engineer
Staff Software Engineer 📱| Увлекаюсь developer experience, open-source и построением сообществ.