Убираем дублирование конфигурации Gradle и при чём здесь косы и горы

—

от автора

Идём к горам

Я Android-разработчик уже несколько лет и работая с монолитами (одномодульными проектами) никакой проблемы с дублированием конфигураций Gradle я не испытывал, ибо файл-то один, однако, когда я начал разбивать свои проекты на модули, стало очевидным копирование одних и тех же настроек и библиотек для различных типов модулей. Более того, одни и те же типы модулей имели практически идентичные зависимости и настройки (например, версию SDK, настройки компиляции и др.).

Я понял, что такое копирование выглядит глупо, и решил сначала найти способ как-то упростить добавление зависимостей. В одной из статей нашёл много способов это сделать, однако единственный приглянувшийся мне — Version Catalogs, это позволяло получать зависимости из единого центра и немного упрощало подключение связанных зависимостей. Но это не решало проблему очевидного дублирования конфигураций, а потому Version Catalogs было не достаточно. Попытки найти способ решения этой проблемы в Google не увенчались успехом. Опрошенные коллеги тоже не предложили способа удобно переиспользовать зависимости с конфигурациями, хотя идеи с конфигурацией модулей были, но мне они показались недостаточными, поэтому я решил придумать что-нибудь самостоятельно. Мне хотелось найти что-то легко реализуемое безо всяких там плагинов, buildSrc и отдельных модулей с зависимостями и настройками.

Так появилась идея КоСоГоРа, которая была опробована на нескольких моих проектах.

Косим горы КоСоГоРом

КоСоГоР — компонентная система горизонтального расширения (да, я заменил «и» на «о» и что ты мне сделаешь? я в другом городе, чтобы звучало лучше). Суть подхода в разбиении зависимостей и конфигураций на компоненты, которые можно подключать независимо.

Компонент — это набор самодостаточных манипуляций, необходимых для конфигурации модуля. Под самодостаточностью я имею ввиду то, что каждый компонент в себе уже имеет все нужные компоненты и манипуляции и не нуждается в их применении где-либо вне этого компонента, но ему допустимо зависеть от определённой конфигурации модуля. Каждый тип компонентов хранится в отдельном файле .gradle, а сам компонент является полем ext, чтобы иметь простой доступ к этим компонентам в любом модуле, и представляет собой замыкание (Closure) с первым аргументом типа Project, к которому и будут применяться необходимые манипуляции. Эти файлы подключаются в основной build.gradle проекта через apply from: 'fileName.gradle'.

Обобщённое объявление компонента:

ext.component_smth = { Project project ->    // манипуляции } // Применение component_smth(project)

Компоненты с аргументами создаются и используются несколько сложнее, ибо нужно будет вернуть Closure<Project>, вызвав первый раз с аргументами, после чего вызвать так же как и без аргументов:

ext.component_withArg = { ArgType arg ->    return { Project project ->        // манипуляции    } } // Применение component_withArg(argValue)(project)

Типы компонентов Android

Конфигурация модуля

Особый компонент, содержащий настройку для типов Gradle модулей: подключает нужные плагины и настраивает сам модуль. Отличается от других компонентов тем, что принимает компоненты и применяет их, а потому это единственный тип компонентов, который вызывается в .gradle-файлах модулей, не считая принимаемых им компонентов с аргументами. Также он должен быть единственным компонентом такого типа в .gradle-файле модуля. 

Пример имени файла с конфигурациями: configs.gradle.

Шаблон именования для конфигураций: config_<whatIsIt> (например, config_android).

В Android типы модулей обусловлены подключаемыми плагинами, которые обычно содержат слово libarary, поэтому шаблон именования для конфигураций модулей Android: library_<whatIsIt> (например, library_android).

Поскольку это особый компонент, его объявление отличается от других типов компонентов:

ext.config_some = { Project project, Closure<Project>... components ->    // настройка для этого типа     for (final def component in components) {        component(project)    } } // или с аргументами ext.config_someWithArgs = { Project project, ArgType arg, Closure<Project>... components ->    // настройка для этого типа с аргументами     for (final def component in components) {        component(project)    } }

В месте использования удобнее отделять типы компонентов по строчкам друг от друга и сортируя их по размеру от большего к меньшему: bundle, component, buildFeature — тогда легче понять, о чём этот модуль. Обобщённое использование выглядит так:

config_some(    project,    component1, component2,    componentOfAnotherType1, componentOfAnotherType2, ) // или с аргументами config_someWithArgs(    project,    argumentValue,    component1, component2,    componentOfAnotherType1, componentOfAnotherType2, ) // или компоненты с аргументами config_some(    project,    component1, component2(argumentValue),    componentOfAnotherType1(argumentValue2), componentOfAnotherType2, )  // остальная конфигурация модуля

Чаще всего в Android необходимы конфигурации Java и Android библиотек. Дополнительно для Android я отдельно выделяю конфигурацию Android параметров (SDK версии, опции компилятора, тип сборки и др.), чтобы её можно было использовать и в :app и в Android-модулях, поскольку они имеют одинаковую структуру конфигурации, а в простых случаях ещё и параметры.

Пример конфигурации Kotlin(Java)-модуля
def javaVersion = JavaVersion.VERSION_17  ext.library_kotlin = { Project project, Closure<Project>... components ->    project.apply plugin: 'java-library'    project.apply plugin: 'org.jetbrains.kotlin.jvm'     project.java {        sourceCompatibility = javaVersion        targetCompatibility = javaVersion    }     for (final def component in components) {        component(project)    } }

Пример конфигурации Android и Android-модуля
def javaVersion = JavaVersion.VERSION_17  ext.library_android = { Project project, Closure<Project>... components ->    project.apply plugin: 'com.android.library'    project.apply plugin: 'org.jetbrains.kotlin.android'     config_android(project, components)     project.android {        defaultConfig {            consumerProguardFiles 'consumer-rules.pro'        }    } }  ext.config_android = { Project project, Closure<Project>... components ->    project.android {        compileSdk sdk         defaultConfig {            minSdk project.minSdk            targetSdk project.sdk             resourceConfigurations = ['en']        }        buildTypes {            debug {                minifyEnabled false            }            release {                minifyEnabled true                proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'            }        }         compileOptions {            sourceCompatibility javaVersion            targetCompatibility javaVersion        }        kotlinOptions {            jvmTarget = javaVersion.toString()        }         lint {            htmlReport false        }        packagingOptions {            resources {                excludes += ['META-INF/ASL2.0', 'META-INF/LICENSE*', 'META-INF/NOTICE', 'META-INF/NOTICE.txt', 'META-INF/MANIFEST.MF']            }        }    }     for (final def component in components) {        component(project)    } }

Поскольку конфигурация Android для большинства Android-модулей одинаковая, то можно использовать основную (config_android), однако можно добавить необходимые параметры в объявление, если необходима более точная настройка. Например targetSdk:

ext.library_android = { Project project, int customTargetSdk, Closure<Project>... modifiers ->    // ...     config_android(project, customTargetSdk, modifiers)     // ... }  ext.config_android = { Project project, int customTargetSdk, Closure<Project>... modifiers ->    project.android {        compileSdk customTargetSdk         defaultConfig {            minSdk project.minSdk            targetSdk customTargetSdk             // ...        }        // ...    }    // ... }

Компонент

Элементарная единица КоСоГоРа. Содержит конфигурацию (например, включение определённых buildFeature) и связанные библиотеки для определённой цели. Хоть он и самодостаточный, однако допустимо использование компонентов с особыми настройками (например, buildFeature_resConfigs), но при использовании других компонентов он уже становится bundle. 

Пример имени файла с компонентами: components.gradle.

Шаблон именования: component_<whatIsIt> (например, component_retrofit).

В Android их можно разделить на Android-компоненты и просто компоненты. Android-компоненты содержат настройки и библиотеки для Android-модулей и не должны применятся в Java-модулях. Для этого я выделяю их с помощью добавления _android к названию, таким образом шаблон именования: component_android_<whatIsIt> (например, component_android_room).

Пример просто компонента
ext.component_retrofit = { Project project ->    project.dependencies {        implementation "com.squareup.retrofit2:retrofit:$retrofitVersion"        implementation "com.squareup.moshi:moshi-kotlin:$moshiVersion"    } }

Пример Android-компонента
ext.component_android_room = { Project project ->    project.apply plugin: 'com.google.devtools.ksp'     project.dependencies {        implementation "androidx.room:room-ktx:$roomVersion"        implementation "androidx.room:room-runtime:$roomVersion"        ksp "androidx.room:room-compiler:$roomVersion"    } }

Bundle

Содержит минимальную конфигурацию для определённого типа модулей (например, для feature). Может использовать все другие типы компонентов кроме конфигураций модуля. Позволяют избежать повторения использования в одинаковом составе компонентов и каких-либо дополнительных манипуляций. Также в каком-то смысле являются маркерами того, о чём этот модуль, поэтому и рекомендую располагать их вверху списка компонентов.

Пример имени файла с bundle: bundles.gradle.

Шаблон именования: bundle_<whatIsIt> (например, bundle_uiScreen). Обычно я не разделяю на Android и нет, как это было с компонентами, поскольку их у меня немного, но думаю это будет единообразнее.

Пример bundle

Bundle для модулей с экранами:

ext.bundle_uiScreen = { Project project ->    component_android_viewBinding(project)    component_android_hilt(project)    buildFeature_resValues(project)     project.dependencies {        implementation "androidx.core:core-ktx:$coreVersion"        implementation "com.google.android.material:material:$materialVersion"        implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycleVersion"        implementation "androidx.navigation:navigation-fragment-ktx:$navigationVersion"         implementation "androidx.fragment:fragment-ktx:$fragmentVersion"    } }

Свои типы компонентов

Иногда в проекте могу появиться особые типы компонентов, которые хочется выделить отдельно, поэтому их можно вынести отдельно в свой файл и именовать по своему. Для единообразия рекомендую придерживаться шаблона <componentName>_<whatIsIt>, а файл именовать <componentNames>.gradle.

Например, я отключаю buildfeatures в настройках gradle и мне нужно их включать в определённых модулях. Такие компоненты я называю buildFeature_<what> (например, buildFeature_resValues) и храню в buildFeatures.gradle.

Характеристика

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

Преимущества

  • Главное преимущество в упрощении конфигурации и удалении её повторяющихся кусков;

  • Можно изменить конфигурацию сразу для всех модулей одновременно;

  • Меньше времени уходит на настройку нового модуля;

  • Достаточно базовых знаний Groovy и Gradle;

  • Больше горизонтальная расширяемость нежели вертикальная;

  • Гибкая настройка компонентов с помощью аргументов.

Недостатки

  • При обновлении Gradle миграция не будет применена, поэтому необходимо применять её вручную;

  • IntelliJ IDEA плохо поддерживает Groovy, поэтому искать исходный код компонентов приходится вручную.

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

Примеры

Kotlin(Java)-модуль

До
plugins {    id 'java-library'    id 'org.jetbrains.kotlin.jvm' }  java {    sourceCompatibility JavaVersion.VERSION_17    targetCompatibility JavaVersion.VERSION_17 }

После
library_kotlin(project)
Компоненты
def javaVersion = JavaVersion.VERSION_17  ext.library_kotlin = { Project project ->    project.apply plugin: 'java-library'    project.apply plugin: 'org.jetbrains.kotlin.jvm'     project.java {        sourceCompatibility = javaVersion        targetCompatibility = javaVersion    } }

Android-модуль экрана

До
plugins {    id 'com.android.library'    id 'org.jetbrains.kotlin.android'     id 'kotlin-kapt'    id 'com.google.dagger.hilt.android' }  android {    namespace 'com.dropdrage.simpleweather.settings.presentation'     compileSdk sdk     defaultConfig {        minSdk project.minSdk        targetSdk project.sdk         resourceConfigurations = ['en']         consumerProguardFiles 'consumer-rules.pro'    }    buildTypes {        debug {            minifyEnabled false        }        release {            minifyEnabled true            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'        }    }    buildFeatures {        resValues true        viewBinding true    }     compileOptions {        sourceCompatibility JavaVersion.VERSION_17        targetCompatibility JavaVersion.VERSION_17    }    kotlinOptions {        jvmTarget = '17'    }     lint {        htmlReport false    }    packagingOptions {        resources {            excludes += ['META-INF/ASL2.0', 'META-INF/LICENSE*', 'META-INF/NOTICE', 'META-INF/NOTICE.txt', 'META-INF/MANIFEST.MF']        }    }     testOptions.unitTests.all {        useJUnitPlatform()    } }  dependencies {    implementation "androidx.core:core-ktx:$coreVersion"    implementation "com.google.android.material:material:$materialVersion"    implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycleVersion"    implementation "androidx.navigation:navigation-fragment-ktx:$navigationVersion"     implementation "androidx.fragment:fragment-ktx:$fragmentVersion"     implementation "com.google.dagger:hilt-android:$hiltVersion"    kapt "com.google.dagger:hilt-compiler:$hiltVersion"     implementation "com.github.kirich1409:viewbindingpropertydelegate-noreflection:$viewBindingVersion"     implementation project(':adapters')    implementation project(':core:style')    implementation project(':common:presentation')    implementation project(':data:settings')     testImplementation "org.junit.jupiter:junit-jupiter-api:$junit5Version"    testImplementation "org.junit.jupiter:junit-jupiter-params:$junit5Version"    testImplementation "org.slf4j:slf4j-simple:$slf4jVersion"    testImplementation "com.google.truth:truth:$truthVersion"    testImplementation "io.mockk:mockk:$mockkVersion"    testImplementation "io.mockk:mockk-android:$mockkVersion"    testImplementation "io.mockk:mockk-agent:$mockkVersion"    testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutinesVersion"    testImplementation "app.cash.turbine:turbine:$turbineVersion"    testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:$junit5Version"     testImplementation testFixtures(project(':common:test')) }

После
library_android(    project,    bundle_uiScreen,    component_tests, )  android {    namespace 'com.dropdrage.simpleweather.settings.presentation' }  dependencies {    implementation project(':adapters')    implementation project(':core:style')    implementation project(':common:presentation')    implementation project(':data:settings')     testImplementation "org.junit.jupiter:junit-jupiter-params:$junit5Version"    testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutinesVersion"    testImplementation "app.cash.turbine:turbine:$turbineVersion"     testImplementation testFixtures(project(':common:test')) }
Компоненты
// Build Features ext.buildFeature_resValues = { Project project ->    project.android {        buildFeatures {            resValues true        }    } } ext.buildFeature_viewBinding = { Project project ->    project.android {        buildFeatures {            viewBinding true        }    } }  // Components ext.component_tests = { Project project ->    project.android {        testOptions.unitTests.all {            useJUnitPlatform()        }    }     project.dependencies {        testImplementation "org.junit.jupiter:junit-jupiter-api:$junit5Version"        testImplementation "org.slf4j:slf4j-simple:$slf4jVersion"        testImplementation "com.google.truth:truth:$truthVersion"        testImplementation "io.mockk:mockk:$mockkVersion"        testImplementation "io.mockk:mockk-android:$mockkVersion"        testImplementation "io.mockk:mockk-agent:$mockkVersion"        testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:$junit5Version"    } }  ext.component_android_viewBinding = { Project project ->    buildFeature_viewBinding(project)    project.dependencies {        implementation "com.github.kirich1409:viewbindingpropertydelegate-noreflection:$viewBindingVersion"    } } ext.component_android_hilt = { Project project ->    project.apply plugin: 'kotlin-kapt'    project.apply plugin: 'com.google.dagger.hilt.android'     project.dependencies {        implementation "com.google.dagger:hilt-android:$hiltVersion"        kapt "com.google.dagger:hilt-compiler:$hiltVersion"    } }  // Bundles ext.bundle_uiScreen = { Project project ->    component_android_viewBinding(project)    component_android_hilt(project)    buildFeature_resValues(project)     project.dependencies {        implementation "androidx.core:core-ktx:$coreVersion"        implementation "com.google.android.material:material:$materialVersion"        implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycleVersion"        implementation "androidx.navigation:navigation-fragment-ktx:$navigationVersion"         implementation "androidx.fragment:fragment-ktx:$fragmentVersion"    } }  // Configs def javaVersion = JavaVersion.VERSION_17  ext.library_android = { Project project, Closure<Project>... components ->    project.apply plugin: 'com.android.library'    project.apply plugin: 'org.jetbrains.kotlin.android'     config_android(project, components)     project.android {        defaultConfig {            consumerProguardFiles 'consumer-rules.pro'        }    } }  ext.config_android = { Project project, Closure<Project>... components ->    project.android {        compileSdk sdk         defaultConfig {            minSdk project.minSdk            targetSdk project.sdk             resourceConfigurations = [‘en’]        }        buildTypes {            debug {                minifyEnabled false            }            release {                minifyEnabled true                proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'            }        }         compileOptions {            sourceCompatibility javaVersion            targetCompatibility javaVersion        }        kotlinOptions {            jvmTarget = javaVersion.toString()        }         lint {            htmlReport false        }        packagingOptions {            resources {                excludes += ['META-INF/ASL2.0', 'META-INF/LICENSE*', 'META-INF/NOTICE', 'META-INF/NOTICE.txt', 'META-INF/MANIFEST.MF']            }        }    }     for (final def component in components) {        component(project)    } }

Заключение

КоСоГоР решает проблему дублирования конфигурации и существенно сокращает сами файлы конфигурации, а также достаточно гибок, чтобы его можно было использовать в различных проектах, однако некоторую долю осторожности всё же стоит соблюдать, как и со многими упрощающими технологиями. В дополнение, он не требует умения написания плагинов и прочих навыков Groovy и Gradle, ибо по сути это тот же самый конфигурационный код, что и обычно, только немного по-другому вызывается.

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

P.S. На данный момент не имею проектов, использующих Kotlin Gradle, так что не знаю, какие могут быть проблемы с реализацией на нём, однако в будущем планирую перевести один из проектов на Kotlin Gradle и дополню.


ссылка на оригинал статьи https://habr.com/ru/articles/748658/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *