[Разработка мобильных приложений, Разработка под Android, Gradle] Конфигурация многомодульных проектов
Автор
Сообщение
news_bot ®
Стаж: 6 лет 9 месяцев
Сообщений: 27286
Предыстория
Иногда, когда я прокрастинирую, я занимаюсь уборкой: чищу стол, раскладываю вещи, прибираюсь в комнате. По сути, привожу окружающую среду в порядок — это заряжает энергией и настраивает на рабочий лад. С программированием у меня та же ситуация, только я чищу проект: провожу рефакторинги, делаю различные инструменты и всячески стараюсь упростить жизнь себе и коллегам.
Некоторое время назад мы в команде Android решили сделать один из наших проектов — Кошелек — многомодульным. Это привело как к ряду преимуществ, так и проблем, одна из которых — необходимость конфигурировать каждый модуль заново. Конечно, можно просто копировать конфигурацию из модуля в модуль, но если мы захотим что-то поменять, то придется перебрать все модули.
Мне это не нравится, команде это не нравится, и вот какие шаги мы предприняли, чтобы упростить нашу жизнь и сделать конфигурации проще в сопровождении.
Первая итерация — вынос версий библиотек
На самом деле это уже было в проекте до меня, и вы, возможно, знаете этот подход. Я часто вижу, как разработчики пользуются им.
Подход заключается в том, что надо выносить версии библиотек в отдельные глобальные свойства проекта, тогда они становятся доступны по всему проекту, что помогает использовать их многократно. Обычно это делается в файле build.gradle на уровне проекта, но иногда эти переменные выносят в отдельный .gradle файл и подключают в основном build.gradle.
Скорее всего, вы уже видели такой код в проекте. В нем нет никакой магии, это просто одно из расширений Gradle под названием ExtraPropertiesExtension. Если кратко, то это просто Map<String, Object>, доступный по имени ext в объектe project, а все остальное — работа как будто с объектом, блоки конфигурации и прочее — магия Gradle. Примеры:
.gradle
.gradle.kts
// creation
ext {
dagger = '2.25.3'
fabric = '1.25.4'
mindk = 17
}
// usage
println(dagger)
println(fabric)
println(mindk)
// creation
val dagger by extra { "2.25.3" }
val fabric by extra { "1.25.4" }
val minSdk by extra { 17 }
// usage
val dagger: String by extra.properties
val fabric: String by extra.properties
val minSdk: Int by extra.properties
Что мне нравится в этом подходе: он крайне простой и помогает версиям не разъезжаться. Но у него есть минусы: надо следить, чтобы разработчики использовали версии из этого набора, и это не сильно упрощает создание новых модулей, потому что всё равно приходится копировать еще много чего.
Кстати, подобного эффекта можно добиться, используя gradle.properties вместо ExtraPropertiesExtension, только будьте осторожны: ваши версии можно будет переопределить при сборке с помощью -P флагов, а если вы обращаетесь к переменной просто по имени в groovy-cкриптах, то gradle.properties заменят и их. Пример с gradle.properties и переопределением:
// grdle.properties
overriden=2
// build.gradle
ext.dagger = 1
ext.overriden = 1
// module/build.gradle
println(rootProject.ext.dagger) // 1
println(dagger) // 1
println(rootProject.ext.overriden)// 1
println(overriden) // 2
Вторая итерация — project.subprojects
Моя любознательность, помноноженная на нежелание копировать код и разбираться с настройкой каждого модуля, привела меня к следующему шагу: я вспомнил, что в корневом build.gradle есть блок, который генерируется по умолчанию — allprojects.
allprojects {
repositories {
google()
jcenter()
}
}
Я сходил в документацию и нашел, что в него можно передать блок кода, который будет конфигурировать этот проект и все вложенные проекты. Но это не совсем то, что было нужно, поэтому я пролистал дальше и нашел subprojects — метод для конфигурации сразу всех вложенных проектов. Пришлось добавить немного проверок, и вот что получилось.
Пример конфигурации модулей через project.subprojects
SPL
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 есть документация по разработке кастомных плагинов, в которой все написано.
Немного разобравшись, я сделал плагин, который может настраивать все что нужно с возможностью изменять при необходимости.
Код плагина
SPL
import org.gradle.api.JavaVersion
import org.gradle.api.Plugin
import org.gradle.api.Project
class 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
}
}
}
Теперь конфигурация нового проекта выглядит как apply plugin: ‘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 — используй для того, чтобы подключать общие модули между собой.
Если у тебя есть что добавить или возразить, пиши в комментарии или ищи меня в соцсетях.
===========
Источник:
habr.com
===========
Похожие новости:
- [Разработка веб-сайтов, Разработка мобильных приложений, Разработка под Android, Карьера в IT-индустрии, Разработка под Windows] Нужен ли еще один сервис (opensource)? (в конце опрос)
- [PostgreSQL, Kotlin, Микросервисы, Kubernetes] Как мы в 2020 году изобретали процесс разработки, отладки и доставки в прод изменений базы данных
- [Разработка мобильных приложений, Тестирование мобильных приложений, Аналитика мобильных приложений, Статистика в IT] Выбор мобильных устройств: пошаговая инструкция для начинающих QA. Часть II
- [Разработка под iOS, Разработка мобильных приложений, Обработка изображений] Обновление Lightroom для iPhone и iPad удалило фото пользователей. Некоторые из них уже не восстановить
- [Open source, Разработка под iOS, Разработка мобильных приложений, Swift] Знакомимся с Needle, системой внедрения зависимостей на Swift
- [Разработка мобильных приложений, Разработка под Android, Тестирование мобильных приложений] Автотесты на Android. Картина целиком
- [Open source, Дизайн мобильных приложений] «Остановите Total Commander!» или главная проблема свободного ПО
- [Разработка мобильных приложений, Разработка под Android, Социальные сети и сообщества] В Android 11 уберут возможность выбора камеры в сторонних приложениях, останется только стандартная
- [Kotlin] Опыт оптимизации вычислений через динамическую генерацию байт-кода JVM
- [Программирование, Разработка мобильных приложений, Dart, Flutter] Сервис на языке Dart: каркас серверного приложения
Теги для поиска: #_razrabotka_mobilnyh_prilozhenij (Разработка мобильных приложений), #_razrabotka_pod_android (Разработка под Android), #_gradle, #_android, #_android_development, #_android_dev, #_kotlin, #_kotlin_android, #_gradle, #_groovy, #_buildsrc, #_blog_kompanii_jandeks.dengi (
Блог компании Яндекс.Деньги
), #_razrabotka_mobilnyh_prilozhenij (
Разработка мобильных приложений
), #_razrabotka_pod_android (
Разработка под Android
), #_gradle
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 23:18
Часовой пояс: UTC + 5
Автор | Сообщение |
---|---|
news_bot ®
Стаж: 6 лет 9 месяцев |
|
Предыстория Иногда, когда я прокрастинирую, я занимаюсь уборкой: чищу стол, раскладываю вещи, прибираюсь в комнате. По сути, привожу окружающую среду в порядок — это заряжает энергией и настраивает на рабочий лад. С программированием у меня та же ситуация, только я чищу проект: провожу рефакторинги, делаю различные инструменты и всячески стараюсь упростить жизнь себе и коллегам. Некоторое время назад мы в команде Android решили сделать один из наших проектов — Кошелек — многомодульным. Это привело как к ряду преимуществ, так и проблем, одна из которых — необходимость конфигурировать каждый модуль заново. Конечно, можно просто копировать конфигурацию из модуля в модуль, но если мы захотим что-то поменять, то придется перебрать все модули. Мне это не нравится, команде это не нравится, и вот какие шаги мы предприняли, чтобы упростить нашу жизнь и сделать конфигурации проще в сопровождении. Первая итерация — вынос версий библиотек На самом деле это уже было в проекте до меня, и вы, возможно, знаете этот подход. Я часто вижу, как разработчики пользуются им. Подход заключается в том, что надо выносить версии библиотек в отдельные глобальные свойства проекта, тогда они становятся доступны по всему проекту, что помогает использовать их многократно. Обычно это делается в файле build.gradle на уровне проекта, но иногда эти переменные выносят в отдельный .gradle файл и подключают в основном build.gradle. Скорее всего, вы уже видели такой код в проекте. В нем нет никакой магии, это просто одно из расширений Gradle под названием ExtraPropertiesExtension. Если кратко, то это просто Map<String, Object>, доступный по имени ext в объектe project, а все остальное — работа как будто с объектом, блоки конфигурации и прочее — магия Gradle. Примеры: .gradle .gradle.kts // creation
ext { dagger = '2.25.3' fabric = '1.25.4' mindk = 17 } // usage println(dagger) println(fabric) println(mindk) // creation
val dagger by extra { "2.25.3" } val fabric by extra { "1.25.4" } val minSdk by extra { 17 } // usage val dagger: String by extra.properties val fabric: String by extra.properties val minSdk: Int by extra.properties Что мне нравится в этом подходе: он крайне простой и помогает версиям не разъезжаться. Но у него есть минусы: надо следить, чтобы разработчики использовали версии из этого набора, и это не сильно упрощает создание новых модулей, потому что всё равно приходится копировать еще много чего. Кстати, подобного эффекта можно добиться, используя gradle.properties вместо ExtraPropertiesExtension, только будьте осторожны: ваши версии можно будет переопределить при сборке с помощью -P флагов, а если вы обращаетесь к переменной просто по имени в groovy-cкриптах, то gradle.properties заменят и их. Пример с gradle.properties и переопределением: // grdle.properties
overriden=2 // build.gradle ext.dagger = 1 ext.overriden = 1 // module/build.gradle println(rootProject.ext.dagger) // 1 println(dagger) // 1 println(rootProject.ext.overriden)// 1 println(overriden) // 2 Вторая итерация — project.subprojects Моя любознательность, помноноженная на нежелание копировать код и разбираться с настройкой каждого модуля, привела меня к следующему шагу: я вспомнил, что в корневом build.gradle есть блок, который генерируется по умолчанию — allprojects. allprojects {
repositories { google() jcenter() } } Я сходил в документацию и нашел, что в него можно передать блок кода, который будет конфигурировать этот проект и все вложенные проекты. Но это не совсем то, что было нужно, поэтому я пролистал дальше и нашел subprojects — метод для конфигурации сразу всех вложенных проектов. Пришлось добавить немного проверок, и вот что получилось. Пример конфигурации модулей через project.subprojectsSPLsubprojects { 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 есть документация по разработке кастомных плагинов, в которой все написано. Немного разобравшись, я сделал плагин, который может настраивать все что нужно с возможностью изменять при необходимости. Код плагинаSPLimport org.gradle.api.JavaVersion
import org.gradle.api.Plugin import org.gradle.api.Project class 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 } } } Теперь конфигурация нового проекта выглядит как apply plugin: ‘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 — используй для того, чтобы подключать общие модули между собой. Если у тебя есть что добавить или возразить, пиши в комментарии или ищи меня в соцсетях. =========== Источник: habr.com =========== Похожие новости:
Блог компании Яндекс.Деньги ), #_razrabotka_mobilnyh_prilozhenij ( Разработка мобильных приложений ), #_razrabotka_pod_android ( Разработка под Android ), #_gradle |
|
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 23:18
Часовой пояс: UTC + 5