Предыстория
Иногда, когда я прокрастинирую, я занимаюсь уборкой: чищу стол, раскладываю вещи, прибираюсь в комнате. По сути, привожу окружающую среду в порядок это заряжает энергией и настраивает на рабочий лад. С программированием у меня та же ситуация, только я чищу проект: провожу рефакторинги, делаю различные инструменты и всячески стараюсь упростить жизнь себе и коллегам.
Некоторое время назад мы в команде Android решили сделать один из наших проектов Кошелек многомодульным. Это привело как к ряду преимуществ, так и проблем, одна из которых необходимость конфигурировать каждый модуль заново. Конечно, можно просто копировать конфигурацию из модуля в модуль, но если мы захотим что-то поменять, то придется перебрать все модули.
Мне это не нравится, команде это не нравится, и вот какие шаги мы предприняли, чтобы упростить нашу жизнь и сделать конфигурации проще в сопровождении.

Первая итерация вынос версий библиотек
На самом деле это уже было в проекте до меня, и вы, возможно, знаете этот подход. Я часто вижу, как разработчики пользуются им.
Подход заключается в том, что надо выносить версии библиотек в отдельные глобальные свойства проекта, тогда они становятся доступны по всему проекту, что помогает использовать их многократно. Обычно это делается в файле build.gradle на уровне проекта, но иногда эти переменные выносят в отдельный .gradle файл и подключают в основном build.gradle.
Скорее всего, вы уже видели такой код в проекте. В нем нет никакой магии, это просто одно из расширений Gradle под названием ExtraPropertiesExtension. Если кратко, то это просто Map<String, Object>, доступный по имени ext в объектe project, а все остальное работа как будто с объектом, блоки конфигурации и прочее магия Gradle. Примеры:
.gradle | .gradle.kts |
---|---|
|
|
Что мне нравится в этом подходе: он крайне простой и помогает версиям не разъезжаться. Но у него есть минусы: надо следить, чтобы разработчики использовали версии из этого набора, и это не сильно упрощает создание новых модулей, потому что всё равно приходится копировать еще много чего.
Кстати, подобного эффекта можно добиться, используя gradle.properties вместо ExtraPropertiesExtension, только будьте осторожны: ваши версии можно будет переопределить при сборке с помощью -P флагов, а если вы обращаетесь к переменной просто по имени в groovy-cкриптах, то gradle.properties заменят и их. Пример с gradle.properties и переопределением:
// grdle.propertiesoverriden=2// build.gradleext.dagger = 1ext.overriden = 1// module/build.gradleprintln(rootProject.ext.dagger) // 1println(dagger) // 1println(rootProject.ext.overriden)// 1println(overriden) // 2
Вторая итерация project.subprojects
Моя любознательность, помноноженная на нежелание копировать код и разбираться с настройкой каждого модуля, привела меня к следующему шагу: я вспомнил, что в корневом build.gradle есть блок, который генерируется по умолчанию allprojects.
allprojects { repositories { google() jcenter() }}
Я сходил в документацию и нашел, что в него можно передать блок кода, который будет конфигурировать этот проект и все вложенные проекты. Но это не совсем то, что было нужно, поэтому я пролистал дальше и нашел subprojects метод для конфигурации сразу всех вложенных проектов. Пришлось добавить немного проверок, и вот что получилось.
subprojects { project -> afterEvaluate { final boolean isAndroidProject = (project.pluginManager.hasPlugin('com.android.application') || project.pluginManager.hasPlugin('com.android.library')) if (isAndroidProject) { apply plugin: 'kotlin-android' apply plugin: 'kotlin-android-extensions' apply plugin: 'kotlin-kapt' android { compileSdkVersion rootProject.ext.compileSdkVersion defaultConfig { minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion vectorDrawables.useSupportLibrary = true } compileOptions { encoding 'UTF-8' sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 } androidExtensions { experimental = true } } } dependencies { if (isAndroidProject) { // android dependencies here } // all subprojects dependencies here } project.tasks .withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile) .all { kotlinOptions.jvmTarget = JavaVersion.VERSION_1_8.toString() } }}
Теперь для любого модуля с подключенным плагином com.android.application или com.android.library мы можем настраивать что угодно: подключаемые плагины, конфигурации плагинов, зависимости.
Все было бы отлично, если бы не пара проблем: если мы захотим в модуле переопределить какие-то параметры, заданные в subprojects, то у нас это не получится, потому что конфигурация модуля происходит до применения subprojects (спасибо afterEvaluate). А еще если мы захотим не применять это автоматическое конфигурирование в отдельных модулях, то в блоке subprojects начнет появляться много дополнительных проверок. Поэтому я стал думать дальше.
Третья итерация buildSrc и plugin
До этого момента я уже несколько раз слышал про buildSrc и видел примеры, в которых buildSrc использовали как альтернативу первому шагу из этой статьи. А еще я слышал про gradle pluginы, поэтому стал копать в этом направлении. Все оказалось очень просто: у Gradle есть документация по разработке кастомных плагинов, в которой все написано.
Немного разобравшись, я сделал плагин, который может настраивать все что нужно с возможностью изменять при необходимости.
import org.gradle.api.JavaVersionimport org.gradle.api.Pluginimport org.gradle.api.Projectclass ModulePlugin implements Plugin<Project> { @Override void apply(Project target) { target.pluginManager.apply("com.android.library") target.pluginManager.apply("kotlin-android") target.pluginManager.apply("kotlin-android-extensions") target.pluginManager.apply("kotlin-kapt") target.android { compileSdkVersion Versions.sdk.compile defaultConfig { minSdkVersion Versions.sdk.min targetSdkVersion Versions.sdk.target javaCompileOptions { annotationProcessorOptions { arguments << ["dagger.gradle.incremental": "true"] } } } // resources prefix: modulename_ resourcePrefix "${target.name.replace("-", "_")}_" lintOptions { baseline "lint-baseline.xml" } compileOptions { encoding 'UTF-8' sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 } testOptions { unitTests { returnDefaultValues true includeAndroidResources true } } } target.repositories { google() mavenCentral() jcenter() // add other repositories here } target.dependencies { implementation Dependencies.dagger.dagger implementation Dependencies.dagger.android kapt Dependencies.dagger.compiler kapt Dependencies.dagger.androidProcessor testImplementation Dependencies.test.junit // add other dependencies here } }}
Теперь конфигурация нового проекта выглядит как applyplugin:ru.yandex.money.module и все. Можно вносить свои дополнения в блок android или dependencies, можно добавлять плагины или настраивать их, но главное, что новый модуль конфигурируется одной строкой, а его конфигурация всегда актуальна и продуктовому разработчику больше не надо думать про настройку.
Из минусов я бы отметил то, что для этого решения нужны дополнительное время и изучение материала, но, с моей точки зрения, оно того стоит. Если вы захотите в будущем выносить плагин, как отдельный проект, то я бы не рекомендовал настраивать зависимости между модулями в плагине.
Важный момент: если вы используете android gradle plugin ниже 4.0, то некоторые вещи очень сложно сделать в kotlin-скриптах по крайней мере, блок android проще конфигурировать в groovy-скриптах. Там есть проблема с тем, что некоторые типы недоступны при компиляции, а groovy динамически типизированный, и ему это не важно =)
Дальше standalone plugin или монорепо
Конечно же, третий шаг это еще не всё. Нет предела совершенству, поэтому есть варианты, куда двигаться дальше.
Первый вариант standalone plugin для gradle. После третьего шага это уже не так сложно: надо создать отдельный проект, перенести туда код и настроить публикацию.
Плюсы: плагин можно шарить между несколькими проектами, что упростит жизнь не в одном проекте, а в экосистеме.
Минусы: версионирование при обновлении плагина придется обновлять и проверять его работоспособность в нескольких проектах сразу, а это может занять время. Кстати, на эту тему у моих коллег из бэкенд-разработки есть отличное решение, ключевое слово modernizer инструмент, который сам ходит по репозиториям и обновляет зависимости. Не буду на этом долго задерживаться, пусть лучше они сами расскажут.
Монорепо это звучит громко, но у меня нет опыта работы с ним, а есть только соображения, что один проект, вроде buildSrc, можно использовать сразу в нескольких других проектах, и это могло бы помочь решить вопрос с версионированием. Если вдруг у тебя есть опыт работы с монорепо, то поделись в комментариях, чтобы я и другие читатели могли что-то узнать про это.
Итого
В новом проекте делай сразу третий шаг buildSrc и plugin проще будет всем, тем более, что код я приложил. А второй шаг project.subprojects используй для того, чтобы подключать общие модули между собой.
Если у тебя есть что добавить или возразить, пиши в комментарии или ищи меня в соцсетях.