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

В статье я расскажу, как мы ускорили синхронизацию и сборку проекта, решили проблему лагающей Android Studio с помощью демоприложений, уменьшили простой и улучшили продуктивность разработчиков. 

Меня зовут Олег Шелякин, я ведущий Android-разработчик в мобильном банке Тинькофф. Статья будет интересна тем, кто работает над многомодульным проектом, где количество модулей перевалило за сотню, время синхронизации измеряется в минутах, а время сборки — в десятках минут.

Проект и проблемы с ним

Знакомьтесь! Проект мобильного банка Тинькофф:

Проект :bank

Количество модулей 

около 1000 

Kotlin

2440 тыс. строк

Java

114 тыс. строк

XML

262 тыс. строк


У нас большой многомодульный проект, все модули находятся в монорепозитории. В качестве билд-системы мы используем Gradle 7.5.1 + AGP 7.2.2 + JVM 11 и пишем код в Android Studio.

При разработке такого крупного проекта у нас появился ряд проблем:

  • медленная Android Studio: code completion, finding usages, navigation и так далее;

  • долгая синхронизация Android Studio Sync;

  • долгая фаза конфигурации проекта;

  • долгая холодная сборка;

  • долгая инкрементальная сборка.

Далее подробно расскажу про каждую из проблем, где буду приводить различные билд-метрики проекта, которые замерял для своей рабочей станции — MacBook Pro 16" 2021, M1 Pro (10 ядер), 32 GB RAM, 512 SSD. Все измерения проводил с помощью gradle-profiler.

Медленная Android Studio. Когда мы пишем код, мы хотим пользоваться автокомплитом, быстро находить все реализации интерфейсов, использовать подсказки доступных методов, но все это требует памяти для Android Studio. Android Studio не особо экономичная в этом плане, скорее прожорливая.

На проекте мы выдаем 8 ГБ для Android Studio и 16 ГБ для gradle daemon. Если выдать gradle меньше памяти, то проект не синхронизируется и не соберется.

До определенного момента студии можно просто выдавать больше памяти: пока проект растет и все вроде бы неплохо, фишки студии работают. Но крупный проект развивается быстро, кодовая база растет, памяти нужно все больше не только для Android Studio, но и для gradle. Android Studio начинает жестко лагать, и уже приходится отказываться от всех фишек, которые она предоставляет. 

Такая история негативно сказывается на скорости разработки. Android Studio — наш основной инструмент, которым мы пользуемся изо дня в день. Хочется, чтобы он не сбоил, а был верным и надежным помощником.

Долгая синхронизация Android Studio Sync. Каждый раз, когда мы добавляем изменения в build.gradle или выполняем git fetch/pull, нам приходится синхронизировать проект.

Синхронизация проекта в Android Studio
Синхронизация проекта в Android Studio

Синхронизация не была бы проблемой, если бы не занимала пять минут, как это происходит у нас. К тому же время стремительно растет с увеличением количества модулей. Это большая проблема, потому что нужно часто синхронизировать проект. Если посчитать, сколько раз в день проводится синхронизация, и умножить это на пять минут, можно очень удивиться, сколько времени уходит впустую.

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

Долгая фаза конфигурации проекта. Рассмотрим gradle build lifecycle.

Есть три фазы: initialization, configuration, execution. Запускаются они последовательно при каждом запуске любой gradle-таски
Есть три фазы: initialization, configuration, execution. Запускаются они последовательно при каждом запуске любой gradle-таски

Фаза initialization у нас занимает всего две секунды, поэтому ее пропустим и перейдем к configuration.

Фаза configuration занимает две минуты и выполняется строго последовательно, модули конфигурируются один за одним. Однако у gradle есть экспериментальная фича Project Isolation, благодаря которой в будущем фаза конфигурации будет параллелиться. Но будущее пока не наступило. 

А еще у gradle есть фича Configuration on demand, которая позволяет конфигурировать только те модули, что будут принимать участие в фазе execution. Например, если запустить таску :bank:build, то сконфигурируются все модули, от которых зависит :bank.

В этом примере в фазе конфигурации будут участвовать все модули проекта. :bank — главный модуль приложения, и он зависит от всех остальных.
В этом примере в фазе конфигурации будут участвовать все модули проекта. :bank — главный модуль приложения, и он зависит от всех остальных.
Если же запустить таску :common:build, то в фазе конфигурации будет участвовать только модуль common, что, очевидно, быстрее, чем конфигурировать весь проект
Если же запустить таску :common:build, то в фазе конфигурации будет участвовать только модуль common, что, очевидно, быстрее, чем конфигурировать весь проект

Время фазы configuration напрямую зависит от количества тасок, которые необходимо сконфигурировать перед запуском фазы execution. В свою очередь, на количество тасок влияет количество подключенных к модулю плагинов. Плагины регистрируют таски для модулей. Например, плагины com.android.application или com.android.library, без которых мы бы не могли собрать наше Android-приложение.

Вывод — чем меньше модулей, тем меньше тасок, чем меньше тасок, тем быстрее фаза конфигурации

Радует, что есть configuration cache, благодаря чему gradle просто пропускает конфигурацию при последующих сборках. Но если мы добавим зависимость к build.gradle, то при последующей сборке фаза конфигурации снова запустится. Это произойдет из-за того, что мы поменяли build.gradle-файл — один из инпутов фазы конфигурации. Ждать ради этого дополнительно к сборке еще две минуты конфигурации — зачем?

Долгая холодная сборка. Под холодной сборкой я подразумеваю выполнение всех gradle-тасок, игнорируя build cache и папку build. То есть полная пересборка проекта. Никаких UP-TO-DATE и FROM-CACHE. 

Я сделал замеры холодной сборки для двух тасок: сompileDebugSources и assembleDebug. Первая таска выполняет гораздо меньше, чем вторая. Если упростить, то выглядит это примерно так.

Разница между сompileDebugSources и assembleDebug
Разница между сompileDebugSources и assembleDebug

сompileDebugSources компилирует файлы с исходным кодом: .kt и .java. На выходе мы получаем .class. Эта таска не конвертирует .class в .dex, не компилирует ресурсы и не упаковывает apk-файл, поэтому выполняется гораздо быстрее.

assembleDebug помимо компиляции исходников, также упаковывает ресурсы, мержит манифесты и т. д. На выходе мы получаем готовый apk-файл, который можно установить на устройство. Это наиболее частый кейс для разработки, потому что чаще всего мы хотим увидеть наши изменения на реальном устройстве или эмуляторе

Для того чтобы запустить холодную сборку, можно добавить флаг --rerun-task:

./gradlew :bank:assembleDebug --rerun-tasks
./gradlew :bank:compileDebugSources --rerun-tasks

Флаг --rerun-task форсит перекомпиляцию всех тасок, игнорируя кэш. Посмотрим на результаты замеров:

Холодная сборка :bank

:bank:assembleDebug --rerun-tasks

30 мин. 10 сек.

:bank:compileDebugSources --rerun-tasks

27 мин. 20 сек.

Холодная сборка — довольно редкий сценарий для повседневной разработки. Однако напрашивается вопрос, зачем нам ждать полчаса сборки для assembleDebug, если мы просто хотим изменить текст кнопочки на экране.

Долгая инкрементальная сборка. Кажется, зачем что-то еще выдумывать, если есть инкрементальная сборка и build cache? Есть столько оптимизаций для gradle, но этого недостаточно. Время инкрементальной сборки зависит от вносимых изменений.

Можем добавить изменения только в модуле фичи, от которого зависит только application, и тогда будет пересобран только модуль фичи и application
Можем добавить изменения только в модуле фичи, от которого зависит только application, и тогда будет пересобран только модуль фичи и application
Можем добавить изменения в common-модуле, от которого зависят много других модулей проекта, и тогда будет пересобран весь проект
Можем добавить изменения в common-модуле, от которого зависят много других модулей проекта, и тогда будет пересобран весь проект

Я сделал замеры для всех сценариев, и вот что получилось:

Инкрементальная сборка :bank

compileDebugSources

 (ABI-изменения в модуле feature)

2 мин. 12 сек.

compileDebugSources

 (non-ABI-изменения в модуле feature)

1 мин. 44 сек.

assembleDebug

 (ABI-изменения в модуле feature)

6 мин. 14 сек. 

assembleDebug

 (non-ABI-изменения в модуле feature)

5 мин. 52 сек. 

compileDebugSources

 (ABI-изменения в модуле common)

4 мин. 33 сек.

compileDebugSources

 (non-ABI-изменения в модуле common)

3 мин. 45 сек.

assembleDebug

 (ABI-изменения в модуле common)

8 мин. 24 сек.

assembleDebug

(non-ABI-изменения в модуле common)

7 мин. 21 сек.

В моем случае время сборки при ABI и non-ABI сильно не различается, потому что фиче-модуль подключается только к application и в самом application используется только один класс из этой фичи. Стоит отметить, что все замеры выполнялись при включенном build cache и configuration cache.

В нашем проекте при инкрементальной сборке таски assembleDebug львиную долю на себя забирает таска mergeProjectDex ≈ 4 мин. Проект имеет большое количество dex-файлов, из-за чего mergeProjectDex такой долгий. Таска mergeProjectDex будет выполняться каждый раз при любых изменениях в коде. Я уже упоминал, что для повседневной разработки выполнение таски assembleDebug — самый частый сценарий, так что в большом проекте инкрементальная сборка не всегда спасает.

Все описанные проблемы имеют негативное последствие — снижение продуктивности разработчиков. Сама разработка становится менее комфортной, Android Studio лагает, синхронизация и сборка долгая, цикл получения результата на девайсе от вносимых изменений сильно замедляется. Баг, который можно было бы пофиксить за пару часов, из-за всех этих проблем фиксится в несколько раз дольше. То же самое касается и разработки фич.

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

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

Каким путем мы пошли в мобильном банке 

У нас в проекте настроен configuration cache, remote build cache, используется nonTransitiveRClass, много чего настроено и оптимизировано. Но все это не дает желанной скорости сборки проекта. 

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

Но удаленная сборка не решает проблему синхронизации проекта. То есть Android Studio Sync будет все такой же долгий. И даже если собирать весь проект целиком на удаленном сервере, это будет все равно довольно долго.

Тогда мы подумали: круто бы было разрабатывать фичи в своей песочнице с минимумом модулей и не страдать от всех этих проблем. Согласитесь — идея ОГОНЬ.

Только представьте: вам не нужно каждый раз синхронизировать и собирать все 1000 модулей в проекте. Достаточно подключить только те модули, что нужны прямо сейчас. Выбор очевиден — работать в проекте со 100 модулями или с 1000.

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

Пример демоприложения

Демоприложение — легковесное приложение с ограниченным набором модулей. 

Разница между :demo и :bank в том, что в :demo подключаются только необходимые модули для разработки конкретной фичи, а в :bank подключаются все модули проекта
Разница между :demo и :bank в том, что в :demo подключаются только необходимые модули для разработки конкретной фичи, а в :bank подключаются все модули проекта

Разница между :demo и :bank в том, что в :demo подключаются только необходимые модули для разработки конкретной фичи, а в :bank подключаются все модули проекта.

Когда мы разрабатываем фичу, нам не нужен весь проект целиком. Мы можем разрабатывать с ограниченным набором модулей, которые нужны фиче, а все остальные модули можно смело отбросить. 

Зачем нам демоприложение, если мы и так можем отдельно от всего проекта запустить какую-нибудь таску на конкретном модуле? Например, запустить тесты только для конкретного feature-модуля:

./gradlew :feature:test

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

Фичи нужно где-то запускать, для этого и нужен application. Запускать в основном приложении — возвращаться к тем же проблемам. Значит, нужно запускать фичи в отдельных приложениях, куда будут подключаться только необходимые модули, а не весь проект целиком. Именно для этого демоприложения и нужны.

Демоприложения позволяют разрабатывать и запускать фичи в изоляции от всего проекта

Демоприложений может быть много. Их можно создавать на фичу или на команду, которая разрабатывает сразу несколько фич. Главное — не подключать ничего лишнего, иначе демка превратится в те самые 1000 модулей.

Первые демоприложения и первые грабли

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

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

Не все демоприложения получили большой прирост к скорости синхронизации, потому что не было инструмента, который бы отключал лишние модули из проекта. Каждый раз синхронизировался весь проект, Android Studio все так же лагала.

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

В итоге определили, что необходимо разработать:

  1. Инструмент по отключению лишних модулей.

  2. Архитектуру и правила зависимостей, чтобы улучшить изоляцию модулей.

  3. Общее решение, чтобы упростить и стандартизировать создание демоприложений.

Отключение лишних модулей

Чтобы ускорить синхронизацию проекта, нужно отключить лишние модули. В Intelij Idea есть возможность выгружать неиспользуемые модули. В Android Studio, начиная с версии Dolphin, такого функционала нет. 

С Android-проектами есть проблемы, например проект не синхронизируется после выгрузки модулей. Говорят, что это работало на Android Studio Chipmunk, но я не проверял. 

Есть другой путь — отключать ненужные модули в settings.gradle. Отключать — это значит просто закомментировать, куда проще решение? Но вручную комментировать несколько сотен модулей — задача не для слабонервных. 

Для этого есть плагин Focus от DropBox, который отключает модули автоматически. Например, мы хотим работать над конкретным фиче-модулем. Этот плагин создает уникальный settings.gradle-файл для нашего фиче-модуля и подключает туда только необходимые модули. Но если подтянуть изменения из репозитория, то может оказаться, что фича стала требовать больше модулей или меньше и тогда нужно заново генерировать уникальный settings.gradle.

Мы пошли другим путем. У нас модули отключаются автоматически, нужно всего лишь указать rootModule-модуль в local.properties. Например:

rootModule=demo-app

После того как мы указали rootModule модули, которые ему необходимы в settings.gradle, все остальные будут отключены. Ничего генерировать не нужно. 

Оба решение имеют плюсы и минусы. Но главное — отключить лишние модули в settings.gradle.

Указав rootModule, может оказаться, что из 1000 модулей для demo-app нужно всего лишь 100. И тогда это победа, все будет просто летать.

Но может случиться и так, что demo-app подключит 500 модулей вместо 100, и тогда мы получим все те же тормоза. Это может произойти из-за того, что подключаемые модули имеют плохие связи с другими модулями, например feature-ui зависит от других feature-ui-модулей. Чем меньше модулей подключается, тем быстрее синхронизация, сборка и шустрее Android Studio.

Стандартных gradle-тасок или средств Android Studio, которые бы сказали, сколько модулей подключается, я не знаю. Поделитесь в комментариях, если знаете такой.

Можно воспользоваться тем же load/unload modules в Intelij Idea — в диалоговом окне увидеть эту цифру.

Но напомню, в свежей Android Studio этого функционала нет. Нужно будет запускать Intelij Idea
Но напомню, в свежей Android Studio этого функционала нет. Нужно будет запускать Intelij Idea

Не хочется каждый раз запускать Intelij Idea ради этого, поэтому мы просто написали для gradle таску, которая выводит количество подключаемых модулей:

:demo-app - 500
+--- :feature-ui - 401
       +--- :legacy-heavy-module - 400
+--- :common-one - 1
+--- :common-two - 1
+--- :common-three - 1
//… и так далее

Анализируя этот отчет, можем понять, какие из зависимостей для demo-app тянут половину модулей проекта. К примеру, здесь feature-ui зависит от legacy-heavy-module. Модуль legacy-heavy-module зависит практически от всех ui-модулей в проекте и должен подключаться только к модулю application. 

Не спрашивайте меня, почему legacy-heavy-module подключает практически все ui-модули, — так исторически сложилось, на то он и legacy. Конечно, этого следует избегать.

Так feature-ui подключает лишние 400 модулей. Если убрать плохую зависимость на legacy-heavy-module, feature-ui станет легче и количество подключаемых модулей к demo-app значительно сократится. Например:

:demo-app - 100
+--- :feature-ui - 1
+--- :common-one - 1
+--- :common-two - 1
+--- :common-three - 1
//… и так далее

Эта таска помогает найти проблемные зависимости. Но еще важно понимать, где правильные зависимости, а где явное нарушение, какие модули могут иметь связи между собой, а какие нет. Здесь мы плавно переходим к фундаментальной проблеме — это архитектура.

Проблемы архитектуры

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

Идеальный вариант — если модуль ни от чего не зависит. Но реальность сурова, и далеко не все модули обладают такой изоляцией. 

Предположим, что в нашей архитектуре есть feature-модули. В feature-модуле находится presentation, domain, data. Приведу пару самых частых кейсов нарушения правил зависимостей и расскажу о последствиях.

Гирлянда из модулей. Это плохо для демоприложений потому, что если мы захотим подключить только feature-a, то сделать этого не сможем, потому что все модули тянутся одной длинной цепочкой. Из-за этого мы будем подключать больше модулей, чем нам необходимо. Поэтому важно выстроить абстракции и изолировать модули.

Гирлянда, потому что фиче-модули последовательно подключают друг друга
Гирлянда, потому что фиче-модули последовательно подключают друг друга

Heavy-модуль. Это еще хуже, чем гирлянда. Выше я приводил уже пример плохих зависимостей, когда demo-app стал подключать 500 модулей вместо 100. Выглядит это примерно так.

Мы всего лишь подключили feature-a к demo-app и тут же получили 500 модулей. Все потому, что feature-a зависит от legacy-heavy-module
Мы всего лишь подключили feature-a к demo-app и тут же получили 500 модулей. Все потому, что feature-a зависит от legacy-heavy-module

Чтобы решить эти проблемы, мы выработали правила зависимостей между модулями согласно нашей архитектуре. Также разработали gradle-таску, которая контролирует эти зависимости и выводит список плохих зависимостей, например:

Smell dependency: [feature-a] should not depend on: [feature-b]


По отчету понятно, что модуль feature-a нарушает правила зависимостей и он не должен зависеть от feature-b. Иначе это приведет к проблеме гирлянды. То же самое касается и проблемы heavy-модуля, таска вывела бы такое предупреждение:

Smell dependency: [feature-a] should not depend on: [legacy-heavy-module]

Зачем нужно общее решение и почему это хорошо

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

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

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

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

  1. Создать модуль демоприложения, назовем его app-example-demo.

  2. Применить convention-плагин. Для демоприложений мы написали свой convention plugin, который упрощает настройку build.gradle и делает ее стандартной для всех.

plugins {
   // 1. Добавить плагин для демоприложений
   id("ru.tinkoff.plugins.demo-app")
}

demoApp {
   // 2. Добавить уникальный application id
   applicationId = "ru.tinkoff.example.demo"
   // 3. Добавить имя приложения
   applicationName = "Example Demo"
}

dependencies {
   // 4. Добавить базовый модуль для демо
   implementation project(':app-demo-base')
   // 5. Добавить необходимые зависимости
}

Модуль app-demo-base содержит application, настроенный AndroidManifest, иконку для приложения, общие экраны: splash, login. Все это инкапсулировано внутри этого модуля, разработчики даже не задумываются о том, что внутри. Вся настройка демоприложения находится в этом модуле.

  1. Реализовать DemoAppInitializer. Для того чтобы сконфигурировать демоприложение, нужно реализовать несколько методов.

class ExampleDemoInitializer : DemoAppInitializer() {

   override val needAuth: Boolean = true

   override fun entryPoint(activity: Activity): Intent =
       Intent(activity, ExampleDemoActivity::class.java)

   override fun initialize(app: Application) { 
       ExmapleFeatureComponentHolder.set(ExmapleFeatureComponent())
   }

}

Флаг needAuth включает или отключает авторизацию для демоприложения. Не всегда нужна авторизация — например, если замокать все запросы к серверу или, может быть, фича доступна без авторизации.

Метод entryPoint — просто точка входа в демоприложение. Первый экран, который отобразится после успешной авторизации, если needAuth = true, или после сплэш-экрана, если needAuth = false.

Метод initialize вызывается в Application.onCreate() и необходим для того, чтобы проинициализировать компоненты, которые нужны только для фичи, но не нужны всем остальным. То есть мы можем настроить специфичное окружение для фичи, которая подключается к демоприложению.

Как я писал выше, в модуле app-demo-base находится application, а в модулях демоприложений его нет. Явно мы нигде не используем DemoAppInitializer.

Но кто тогда использует DemoAppInitializer?

DemoAppInitializer — это Initializer из библиотеки AppStartup. Нам нужно добавить его в AndroidManifest демоприложения, дальше AppStartup все сделает за нас. 

DemoAppInitializer будет проинициализирован на этапе старта Application, и все его методы будут вызваны в определенном порядке. Это можно реализовать по-разному, например через ServiceLoader получать зарегистрированный интерфейс конфигурации в Application классе. Но для Android уже есть нативный способ для инициализации компонентов. Им и воспользовались.

  1. Добавить ExampleDemoInitializer в AndroidManifest, для того чтобы AppStartup смог его найти и проиницилизировать. 

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
   xmlns:tools="http://schemas.android.com/tools"
   package="ru.tinkoff.mb.example.demo">

   <application>
       <provider
           android:name="androidx.startup.InitializationProvider"
           android:authorities="${applicationId}.androidx-startup"
           android:exported="false"
           tools:node="merge">
           <meta-data
               android:name="ru.tinkoff.mb.example.demo.ExampleDemoInitializer"
               android:value="androidx.startup" />
       </provider>
   </application>

</manifest>
  1. Отключить лишние модули. В нашем случае это просто указать rootModule в local.properties.

rootModule=demo-app

Настройка завершена, можно разрабатывать свои фичи очень быстро. Еще мы реализовали плагин для AndroidStudio, который все делает за нас. Разработчикам остается только сконфигурировать демоприложение в DemoAppInitializer.

Благодаря общему решению:

  • мы можем добавлять новые фичи для демок и раскатывать сразу на всех;

  • у нас есть единый подход, нет разношерстных подходов и дублирования;

  • стало легче создавать демоприложения;

  • если будет обнаружен баг в общем решении, то фиксить придется только в одном месте;

  • у нас есть светлое будущее по расширению функционала.

Результаты, которых мы достигли, используя демоприложения:

:bank

vs

:demo

1000

Количество модулей

200

5 мин. 42 сек.

Android Studio Sync

40 сек.

1 мин. 56 сек.

Конфигурация

15 сек.

30 мин. 10 сек.

assembleDebug

 (холодная сборка)

3 мин. 36 сек.

27 мин. 20 сек.

compileDebugSources

 (холодная сборка)

3 мин. 20 сек.

2 мин. 12 сек.

compileDebugSources

 (ABI-изменения в модуле feature)

56 сек.

1 мин. 44 сек.

compileDebugSources

 (non-ABI-изменения в модуле feature)

46 сек.

6 мин. 14 сек.

assembleDebug

 (ABI-изменения в модуле feature)

1 мин. 9 сек.

5 мин. 52 сек. 

assembleDebug

(non-ABI-изменения в модуле feature)

52 сек.

4 мин. 33 сек.

compileDebugSources

 (ABI-изменения в модуле common)

1 мин. 13 сек.

3 мин. 45 сек.

compileDebugSources

 (non-ABI-изменения в модуле common)

1 мин. 1 сек.

8 мин. 24 сек.

assembleDebug

 (ABI-изменения в модуле common)

1 мин. 19 сек.

7 мин. 21 сек.

assembleDebug

(non-ABI-изменения в модуле common)

1 мин. 9 сек.

По всем параметрам проект :demo сильно выигрывает у проекта :bank. 

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

Заключение

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

У нас команды большую часть времени разрабатывают свой функционал в демоприложениях, подключая только модули, которые необходимы. Благодаря этому разработчики не испытывают проблем со скоростью сборки и лагающей Android Studio.

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

Надеюсь, эта статья была интересной и полезной. Пишите в комментариях, если есть вопросы или предложения! 

На прощание несколько ссылок на тему компиляции kotlin/java:

Комментарии (10)


  1. quaer
    17.07.2023 08:52
    +1

    Инкрементальная сборка и холодная дают одинаковый артифакт? Часто инкрементальная даёт бОльший по размеру результат

    Android Studio — наш основной инструмент, которым мы пользуемся изо дня в день. Хочется, чтобы он не сбоил, а был верным и надежным помощником.

    медленная Android Studio: code completion, finding usages, navigation и так далее;

    Android Studio не особо экономичная в этом плане, скорее прожорливая.

    и лагающей Android Studio.

    Отличная реклама Kotlin и описание состояния современного программирования, когда программисты для себя нормальный продукт создать не могут. Ещё и глючный. А выход в релиз сразу с критичными и выдимыми багами, останаваливающими работу, вообще заставляет задаваться вопросом где же все эти лучшие практики программирования.


    1. Oleg_Shelyakin Автор
      17.07.2023 08:52

      Размер артефактов не измерял.

      По инструментам отвечю. Android Studio и gradle имеют свои проблемы влияющие на скорость сборки или количество потребляемой памяти. И есть множество оптимизаций, оптимизаций, которые позволяют ускорить сборку. Спасибо разработчиком этих инструментов за это.

      Но есть также и другая сторона - это проекты с неправильными зависимостями между модулями. Когда все переплетено в связях и нет четких границ у модулей, это приводит к тому, что при каждом чихе пересобирается пол проекта.

      Однозначно нужно следить за архитектурой проекта и развязывать зависимости. За нас эту работу Android Studio или gradle не сделают.


  1. midery
    17.07.2023 08:52
    +1

    Спасибо больше за статью, в русскоязычном сегменте не так много материалов по тому, как правильно готовить демки. Круто, что вы решили его подготовить.

    Мне в статье не хватило конкретных проблем и историй о том, как вы их решали и поддерживали корректность работы. Очень часто в демо-приложениях "болит" конфигурация DI, бойлерплейт, некорректное подключение gradle-зависимостей, формирование корректного стейта приложения для конкретного экрана, демо-аппы для флоу vs атомарные демо-аппы для фич, Было бы круто это так же описать и рассказать. По моему опыту, это не только проблемы вашего проекта, они возникают у всех, кто реализует этот подход)

    Мы в Avito начали делать демо-приложения еще 3 года назад, и столкнулись с многими проблемами, которые вы описали: heavy-модули, тянущие за собой огромное количество реализаций, транзитивное подключение цепочек модулей, и так далее.

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

    Еще интересные инсайты по тому, как организовать многомодульный проект и следить за корректностью связей рассказывают Tinder в своем докладе по развязыванию структуры модулей. При работе с демками очень важна валидация корректной структуры зависимостей, потому что если они будут


    1. Oleg_Shelyakin Автор
      17.07.2023 08:52

      Спасибо за подробный уоммент и полезные ссылки)

      Проблем было много и еще много осталось. Например одна из проблем это общий модуль с бд.

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

      По некорректному подключению gradle завичимостей у нас есть gradle plugin. Он регает таску, которая ругается если что то подключили не туда или выводит список модулей, которые стоит почистить от неправильных зависимостей прежде всего, чтоб уменьшить модули в демке. Кстати плагин хотим заоупенсорсить.

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

      Про стейт приложения не совсем понял. Могли бы детальнее расписать?


  1. princeparadoxes
    17.07.2023 08:52

    А не размышляли над тем, чтобы уменьшить количество подключаемых модулей к demo-приложению через моки?

    Все зависимости, реальная работа которых не очень важна для функциональности demo-приложения и его логики, можно банально замокать через, например, MockK.

    mockk<BduCustomizeDependencies>(relaxed = true)

    Выглядит как костыль, не спорю, но это работает. Правда, при условии, что у вас фичи делятся на api/(impl, ui, bl) модули.

    В нашем случае это помогало избегать подключения аналогов :legacy-heavy-module. Что резко сокращало количество подключаемых модулей.


    1. midery
      17.07.2023 08:52
      +1

      Похожая схема как раз описывается в докладе Square, и там называется fake-модулями.

      У каждой фичи есть feature:public-часть, которая содержит только публичные интерфейсы. Реальные имплементации этих интерфейсов находятся в feature:impl, а фейковые - в feature:fake:

      С такой структурой модулей настраивать различные степени валидации и проверки для демо-приложений. Например, если для критична атомарность - каждое демо-приложение может подключить в себя только одну реальную фичу, а все остальные реализации public-модулей должны быть фейками.

      В Avito мы в данный момент адоптим этот подход, и переходим на структуру модулей с public/impl/fake, одновременно выправляя зависимости демок.


      1. Oleg_Shelyakin Автор
        17.07.2023 08:52

        Спасибо за детальный коммент)

        Мы также подключаем api модули с минимальным количеством зависимостей. Fake это видимо что то по типу заглушек без реальной реализации. У нас такие вещи в test fixtures, отдельный модуль не создаем для этого. Но это вопрос реализации, кому как удобнее. Отдельный модуль с фейками тоже ок, главное его не подключать к модулю app и убирать из settings.gradle чтоб не синкать, а подключать только в settings.gradle для демки.


    1. Oleg_Shelyakin Автор
      17.07.2023 08:52

      Движемся как раз в сторону api/impl. Пока есть горстка модулей без api.

      Для уже существующих модулей api так и делаем, мокаем/стабаем все лишнее.


  1. LuigiVampa
    17.07.2023 08:52
    +1

    Спасибо за интересную статью. Тема крайне актуальная и важная для крупных проектов. Тоже пытался по разному подступиться к этой проблеме на рабочем проекте (550+ gradle модулей, 2М+ LOC), делал оптимизацию сборки, дружащие с местным DI стабы -impl модулей (разбиение на -api, -impl, -stub), но взрывного эффекта, это, конечно, всё равно не давало.

    По моему опыту работы с Gradle, он очень плохо переносит большое количество модулей, каждый новый модуль добавляет существенный импакт на время сборки, даже если это просто -stub модуль с "пустыми" реализациями интерфейсов из -api, количество модулей, к сожалению не меняется, время конфигурации (существенное узкое место, особенно актуальное в "горячих" сборках) не уменьшается, configuration-cache не всегда адекватно работает из-за некоторых подключённых к сборке плагинов, а выигрыш на тасках сборки модуля есть, но не огромный.

    Наиболее правильный подход - это конечно вот такие вот демо, к которым можно подключить только 1-2 разрабатываемых в настоящий момент модуля и не страдать с пересборкой всего проекта. Я, к сожалению, не осилил у себя декомпозировать сильно старые базовые модули, авторизацию и проход до главного экрана, которые в своё время были очень монолитными и завязанными друг на друга, а потом стало слишком больно и сложно их рефакторить.

    Спасибо ещё раз!


    1. Oleg_Shelyakin Автор
      17.07.2023 08:52

      Спасибо за такой развернутый комментарий!