
У нас в проекте с незапамятных времён для DI используется Dagger. И в целом он нас всем устраивает. Ну, разве что кроме одного маленького пунктика — скорость сборки с kapt. Он прилично увеличивает время сборки. Казалось бы, смирись, страдай, прими ситуацию. Но относительно недавно Yandex представили библиотеку для Dependency Injection. Имя ей Yatagan. У неё есть две важных особенности — она спроектирована быть похожей на Dagger по API, и одна из её целей — меньше влиять на время сборки. Меньшее время сборки — это всегда хорошо, а значит, стоит её как минимум попробовать.
По всем правилам приличия представлюсь — меня зовут Перевалов Данил, а теперь давайте перейдём к теме.
А как сейчас?
У нас среднего размера проект, и я уже как-то писал про измерения времени сборки в нём. Методология измерений и сценарии будут оттуда же, почти полностью. Кратко:
-
В качестве инструмента измерения выступает Gradle Profiler.
-
6 прогонов. Первый для «прогрева», а потом берём медианный из оставшихся пяти.
-
Сценария два:
-
Холодная сборка. Делаем clean assembleDebug —no-build-cache.
-
Горячая сборка. Изменяем один файл в фичёвом модуле, в котором есть Dagger, и делаем assembleDebug.
-
-
Включён Configuration Cache, так как хочется оценить влияние именно на build составляющую.
-
Проект: 479 Gradle модулей, 813 тысяч строк Kotlin, 53 тысячи строк Java и 136 тысяч строк XML.
-
И самое главное в нём — 528 Dagger компонентов.
Перед началом внедрения стоит понять, как обстоит ситуация сейчас. Замеряем время сборки с Dagger 2.

Не слишком быстро, но и не слишком долго. Нас в целом устраивает.
А в чём суть?
Yatagan позиционирует себя как замену Dagger2. У него очень схожее API. На первый взгляд совпадает процентов на 90%. Зачастую достаточно автозаменой просто поменять package в импортах.
Так за счёт чего вообще планируется получить уменьшение времени сборки? Есть несколько факторов:
-
Yatagan не генерирует Factory для всех зависимостей в отличие от Dagger. Меньше кода — быстрее работа и быстрее генерация.
-
Yatagan имеет возможность работать без кодогенерации вообще. То есть DI полностью работает на рефлексии. Профит в том, чтобы для debug сборок, которые разработчики собирают во время собственно разработки, использовать рефлексию, а для билда для тестировщиков и релиза — уже кодогенерацию.
-
Yatagan имеет два способа кодогенерации: ksp и kapt. KSP в теории должен генерировать быстрее, так как у kapt есть неприятная особенность — он генерирует так называемые стабы. Это отдельная задача, которая сама по себе отнимает много времени сборки.
Очень важный плюс Yatagan, что не нужно сразу переходить на использование рефлексии и KSP. Можно переходить мягко и гладко. Сначала просто перейти на Yatagan с kapt, потом запариться с рефлексией и в самом конце уже распробовать KSP.
Поэтому я решил быстро и на коленке попробовать внедрить Yatagan, померить потенциальный профит, понять основные проблемы, обсудить с командой, и если всё понравится, внедрять на уровне компании.
Внедряем
Собственно, изначальный план следующий:
-
Внедряем Yatagan просто как замену Dagger. Замеряем.
-
Внедряем использование рефлексии для debug сборок. Замеряем.
-
Внедряем KSP для кодогенерации. Замеряем.
Далее я буду рассказывать в основном о проблемах внедрения. Это не значит, что библиотека плохая или сложная. Просто хочется рассказать, с какими проблемами вы можете столкнуться при миграции.
Ну и, естественно, я опишу только наш путь. Поэтому некоторые проблемы будут весьма специфичны. А с некоторыми проблемами, которые могут возникнуть у вас, я не столкнулся.
Внедряем просто как замену
Начнём с самого первого шага и очевидного шага — в build.gradle поменять зависимости с Dagger на Yatagan. У нас все зависимости и их версии описаны в отдельном файле, а в builde.gradle подключается константа. Так что этот шаг для меня заключался в смене значения двух строк. Начало позитивное.
Дальше просто последуем инструкции Migration from Dagger с Github страницы библиотеки.
В ней довольно простые шаги. Где-то нужно поменять package у импортов, а где-то поменять имена аннотаций. Главное, что объединяет почти все пункты — их можно сделать обычной глобальной автозаменой в Android Studio.
Почти все, кроме одного… Самый вредный и плохой пункт «Replace DaggerMyComponent.builder() with Yatagan.builder(MyComponent.Builder::class.java) or similar.».
Когда с ходу читаешь гайд по миграции, этот пункт не особо бросается в глаза. В чём суть? Когда вы написали какой-либо Component, например:
@Component( dependencies = [ RouterDependencies::class ], modules = [ CrashModule::class ] ) interface CrashComponent
То Dagger заботливо генерирует для вас не только DaggerCrashComponent, но и класс Builder, который нужен для создания компонента. Для обращения к сгенерированному Builder достаточно вызвать статический метод builder у сгенерированного компонента.
fun build(routerDependencies: RouterDependencies, crashModule: CrashModule) { DaggerCrashComponent.builder() .routerDependencies(routerDependencies) .crashModule(crashModule) .build() }
Yatagan же требует иного подхода, чтобы у каждого компонента был руками прописан интерфейс с аннотацией Component.Builder, на основе которого и будет сгенерирован Builder.
@Component( dependencies = [ RouterDependencies::class ], modules = [ CrashModule::class ] ) interface CrashComponent { @Component.Builder interface Builder { fun routerDependencies(dep: RouterDependencies): Builder fun crashModule(mod: CrashModule): Builder fun build(): CrashComponent } }
У нас ни у одного из компонентов такого интерфейса нет. Мы всё время пользовались тем, что генерировал Dagger. Более того, у нас есть KSP генератор, который использует сгенерированные Dagger Builder’ы, чтобы не писать кучу boilerplate кода при создании экземпляра компонента.
Как я понял, требование интерфейса с аннотацией Component.Builder упирается в использование рефлексии. Так как не происходит генерации, а создавать Component по какому-то правилу надо.
Поэтому в рамках моего эксперимента мне пришлось по-быстрому накидать скрипт, который пробегает по всем файлам, содержащих в названии Component. И дальше на основе аннотации Component генерирует interface с аннотацией Component.Builder.
Быстрое и рабочее решение. Хотя оно мне не по вкусу. При внедрении в, так сказать, production код я бы сделал по-другому. Так как даже простой Dagger компонент начинает выглядеть странно:
@PerPresentationScope @Component( dependencies = [ RouterDependencies::class ], modules = [ CrashModule::class ] ) interface CrashComponent { fun inject(graph: CrashGraph) @Component.Builder interface Builder { fun routerDependencies(dep: RouterDependencies): Builder fun crashModule(mod: CrashModule): Builder fun build(): CrashComponent } companion object { fun build( routerDependencies: RouterDependencies, crashModule: CrashModule ) { DaggerCrashComponent.builder() .routerDependencies(routerDependencies) .crashModule(crashModule) .build() } } }
Как будто один и тот же код про RouterDependencies и CrashModule повторили четыре раза. А ведь у нас есть компонент, у которого 61 зависимость. Если умножить на 4, то получится какое-то неприличное количество строк для DI-компонента. Это, конечно, исключительный случай, но всё же.
Поэтому в идеале стоит попробовать расширить Yatagan, добавив возможность работы с чем-то вроде менеджера зависимостей. Который бы работал только по информации из аннотации Component.
Помимо этого, пришлось перевести работу нашего KSP генератора на работу с Yatagan и interface Component.Builder.
Больше в проекте ничего красным не подсвечивалось, поэтому пробую собрать и… Конечно же, ошибка. Выявилась ещё пара проблем.
Во-первых, Yatagan не хочет работать с аннотацией Binds, если у неё есть Scope, сообщая, что надо ставить аннотацию Scope над конкретным классом или Provides. Dagger позволяет подобные выкрутасы. Поэтому я по-быстрому перенёс Scope на реализации.
Во-вторых, как было сказано выше, Yatagan не генерирует Factory. У нас в своё время Dagger Module на Java имели package local методы, и местами это осталось. Благо, что немного. С Factory это не вызывает проблем, так как Factory генерируется в этом же пакете. А вот если Factory нет, то надо либо перевести Dagger Module на Kotlin, где использовать модификатор internal, либо добавить модификатор public в Java коде. Я выбрал второй вариант, так как это быстрее.
После этого всё. Наконец-то заработало. Начинаем замерять.

Минус 5% для горячей сборки и аж минус 17% для холодной. Вполне отличный результат для замены всего одной библиотеки.
Но двигаемся дальше.
Внедряем рефлексию для debug
Теперь настало время попробовать внедрить рефлексию. По идее, всё должно было быть просто: заменяем зависимости в build.gradle. Вместо:
implementation("com.yandex.yatagan:api-compiled:1.1.0") kapt("com.yandex.yatagan:processor-jap:1.1.0")
Везде вставляем следующую конструкцию:
debugImplementation("com.yandex.yatagan:api-dynamic:1.1.0") releaseImplementation("com.yandex.yatagan:api-compiled:1.1.0") kaptRelease("com.yandex.yatagan:processor-jap:1.1.0")
Звучит просто. С ходу, конечно же, ничего не заработало. Оказалось, что у нас в проекте Dagger не везде был подключен красиво и правильно. В некоторых модулях он был подключен явно не к месту, в других было подключение только kapt, а сама библиотека подтягивалась через api от других модулей, а в третьих было и implementation, и kapt. Ни сборке, ни в работе это не мешает, но это тот исключительный случай «Не думаю, что стоит заморачиваться с выносом этого в отдельное место. Маловероятно, что нам когда-то по-быстрому надо будет поменять DI на всё проекте разом».
Пришлось немного покопаться и в специальном модуле base-di, в котором у нас лежат разные примочки для DI, объявить:
debugApi("com.yandex.yatagan:api-dynamic:1.1.0") releaseApi("com.yandex.yatagan:api-compiled:1.1.0")
При этом из всех остальных модулей подключение библиотеки убрать, оставив только kapt.
kaptRelease("com.yandex.yatagan:processor-jap:1.1.0")
Теперь всё завелось. Начинаем тесты.

Результат… Ну, такое… Не слишком впечатляет. И причина тут в том, что плагин kapt никуда не делся, а значит, стабы всё равно генерируются. По сути, просто ушла лишь генерация самого Yatagan, а он, видимо, и так очень быстрый.
Это было ожидаемо. Подключение через рефлексию даст максимальный профит в случае, если ваш проект на Java и вместо kapt вы используете annotationProcessor, ну или Dagger последняя библиотека, которая использует kapt. Я бы сказал, что такой подход является заменой Dagger Reflect, который уже давненько не поддерживается.
Поэтому данный этап для меня скорее был переходным к следующему шагу — внедрению KSP.
Используем KSP
Я прошёлся по проекту автозаменой и поменял kaptRelease на kspRelease. Ну и пришлось пробежаться по всем четырём с лишним сотням build.gradle файлов чтобы убрать kapt там, где он стал не нужен, и подключить KSP там, где он стал нужен. Также оказалось, что местами плагин kapt был подключен там, где и не был нужен. Видимо, неудачно скопипастили код build.gradle, в таким местах смело его убираем.
Это была муторная, но простая работа, во время выполнения которой я осознал страшный и неприятный факт: kapt всё ещё нужен большому количеству модулей. Библиотек также использующих kapt оказалось целый три:
-
Realm. Остался у нас в паре модулей. Планируем в ближайшее время избавиться от него полностью.
-
Room. Сейчас у нас использует kapt. Он подключен в небольшом количестве модулей. Имеет KSP плагин, просто надо на него перейти.
-
Moxy. И вот это проблема. На новый ViewModel + Compose мы перешли недавно, а значит, 90% экранов всё ещё используют Moxy, а ему нужен kapt.
Как следствие, избавиться от kapt удалось только в трети модулей, которые и не очень-то большие по количеству кода. Я, конечно, расстроился, но решил продолжить. Уж больно хотелось взглянуть на результаты.

Для горячей сборки результат действительно улучшился. Это из-за того, что модуль, в котором вносится изменение, написан уже на новой архитектуре и не содержит Moxy.
А вот для холодной… Результаты даже хуже, чем с kapt. Из-за использования Moxy стабы продолжают генерироваться для большей части кода. Так сверху ещё и KSP плагин добавили.
Чувство недосказанности
Возможно, на данном этапе вы подумали: «И это всё? Moxy не даёт использовать возможности KSP на полную и ничего не сделать? Ты с этим ничего не сделаешь, тряпка?». У меня на душе к этому этапу оставалось гаденькое чувство, что работа не закончена. Поэтому я решил попробовать симулировать ситуацию, когда у мы полностью избавились от kapt, либо кто-то сделал для неё KSP плагин.
Для этого просто выпиливаю весь kapt, кроме мест, где он необходим для Realm. Но это буквально пара модулей и без этого никак. Да, проект будет падать при запуске, но нам это и не важно в данной ситуации. Важно, что он соберётся и можно сделать замеры.
Поэтому автозаменой Android Studio заменяю подключение kapt на пустую строку, вспоминаю про Realm и возвращаю kapt в двух местах.
Теперь можно делать тесты. Результат получился следующим:

Для горячей, само собой, почти ничего не изменилось, а вот для холодной результаты весьма впечатляющие. На целых полторы минуты меньше, чем сборка с Dagger. Что в процентах значит, что сборка стала быстрее на четверть.
Что в итоге?
После моего маленького исследования мы собрались командой и обсудили внедрение Yatagan уже как production решения.
По факту для нас полезны два вида перехода:
-
Внедрение просто как замена Dagger.
-
Внедрение с KSP и рефлексией.
Второй вариант пока точно не реализуем с раскрытием всего своего потенциала, так как отказываться от kapt — дело явно не быстрое и не простое.
Первый вариант, конечно, уменьшает время сборки на 17%, но несёт в себе большой спектр работ, и как-то нужно доработать библиотеку под нас, чтобы не писать Component.Builder, а также есть некоторые риски и нюансы:
-
Библиотека пока не популярная, могут быть проблемы с поддержкой, мало документации.
-
Сильно улучшилось время именно холодной сборки и это хорошо для CI. Горячая же, а именно её ежедневно по сотне раз собирают разработчики, ускорилась не так сильно – на 5%. А в абсолютных цифрах это вообще 3 секунды.
-
Есть опасения, что «овчинка выделки не стоит». Лучше дождаться, пока библиотека «подсохнет». Есть риск собрать грабли, которые сведут на нет все преимущества от ускорения сборки.
-
Время сборки нас сейчас не сильно тревожит, так как минута — неплохой результат. Иногда установка APK на телефон с micro-USB занимает больше времени.
-
Риски со стороны соискателей при поиске работы. Dagger и так не самая простая библиотека, а тут ещё и просто похожее решение.
И по итогу мы решили отложить решение по внедрению до конца года. Там посмотреть на ситуацию и если что-то улучшится, запланировать на следующий год.
Само собой, для вас часть проблем может быть неактуальна. Тут лишь наша история, но я надеюсь, она поможет кому-то прикинуть наши результаты на свой проект и сэкономит время на собственные исследования, чтобы принять решение о переходе или полном отказе от этой идеи. Тут уж на ваше усмотрение.
ссылка на оригинал статьи https://habr.com/ru/articles/721248/
Добавить комментарий