
Введение
В многомодульных приложениях Android существует проблема организации зависимости gradle. Каждая зависимость указывается отдельно. Примерно вот так
dependencies {
    implementation("androidx.core:core-ktx:1.13.1")
    implementation("androidx.appcompat:appcompat:1.7.0")
    implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.8.4")
    implementation("androidx.activity:activity-compose:1.9.1")
    implementation(platform("androidx.compose:compose-bom:2024.08.00"))
    implementation("androidx.compose.ui:ui")
    implementation("androidx.compose.ui:ui-graphics")
    implementation("androidx.compose.ui:ui-tooling-preview")
    implementation("androidx.compose.material3:material3")
    implementation("androidx.navigation:navigation-compose:2.8.0")
    debugImplementation("androidx.compose.ui:ui-tooling")
    implementation("com.google.dagger:hilt-android:2.51.1")
    kapt("com.google.dagger:hilt-android-compiler:2.51.1")
    kapt("androidx.hilt:hilt-compiler:1.2.0")
    implementation(project(":mymodule"))
    
    ...
  }Конечно в реальных проектах зависимостей может быть больше в разы. В данном примере я лишь хочу продемонстрировать Декларативный Подход в организации зависимостей. Проблема в том что, из модуля в модуль приходится дублировать такой код. Конечно не забывая проверить, так как зависимости могут быть различными в каждом модуле.
Зависимости могут конфликтовать друг с другом или применяться различные версии. Что, очевидно, не хорошо.
Конечно есть решения, которые немного облегчают написание подобного кода.
Это описание зависимостей в toml файле или вынесение зависимостей в глобальные переменные с помощью Groovy или Kotlin Dsl. 
После применения этих подходов код будет выглядеть вот так:
dependencies {
    implementation(libs.androidx.core.ktx)
    implementation(libs.androidx.appcompat)
    implementation(libs.androidx.lifecycle.runtime.ktx)
    implementation(libs.composeActivity)
    implementation(libs.composeBom)
    implementation(libs.androidx.ui)
    implementation(libs.androidx.ui.graphics)
    implementation(libs.androidx.ui.tooling.preview)
    implementation(libs.androidx.material3)
    implementation(libs.composeNavigation)
    debugImplementation(libs.androidx.ui.tooling)
    implementation(libs.hilt.android)
    kapt(libs.hilt.android.compiler)
    kapt(libs.androidx.hilt.compiler)
    implementation(project(":mymodule"))
    ...
  
  }
Но, по моему мнению, подобное решение не решает проблему процедурной организации зависимостей. 
Стало лучше? Ответ — нет. Да, мы решили проблему конфликтов. И теперь зависимости вынесены в глобальные переменные. Но это не решило проблему дублирования кода. А также код у нас по‑прежнему написан в процедурном стиле. Мы подключаем зависимости одну за одной. Плюс каждый модуль получает абсолютную свободу в подключении зависимостей. Давайте ее немного ограничим.
Пример будет показан с применением Kotlin Dsl, но это не принципиально. Аналогичного результата можно достичь и с помощью Groovy gradle.
Добавим extension в модуль Kotlin Dsl:
import org.gradle.api.artifacts.Dependency
import org.gradle.api.artifacts.dsl.DependencyHandler
fun DependencyHandler.implementation(dependency: String) {
    add("implementation", dependency)
}
fun DependencyHandler.implementation(dependency: Dependency) {
    add("implementation", dependency)
}
fun DependencyHandler.kapt(dependency: String) {
    add("kapt", dependency)
}
fun DependencyHandler.testImplementation(dependency: String) {
    add("testImplementation", dependency)
}
fun DependencyHandler.androidTestImplementation(dependency: String) {
    add("androidTestImplementation", dependency)
}
fun DependencyHandler.androidTestImplementation(dependency: Dependency) {
    add("androidTestImplementation", dependency)
}
fun DependencyHandler.debugImplementation(dependency: String) {
    add("debugImplementation", dependency)
}Возможно, список extension не полный. Но вы можете легко его дополнить или изменить.
Теперь создадим extension зависимостей:
fun DependencyHandler.Android() {
    implementation(AppDependencies.Android.androidxCooreKtx)
    implementation(AppDependencies.Android.androidxAppcompat)
    implementation(AppDependencies.Android.androidxLifecycleRuntimeKtx)
}
fun DependencyHandler.Compose() {
    implementation(AppDependencies.Compose.composeActivity)
    implementation(platform(AppDependencies.Compose.composeBom))
    implementation(AppDependencies.Compose.composeUi)
    implementation(AppDependencies.Compose.composeUiGraphics)
    implementation(AppDependencies.Compose.composeUiToolingPreview)
    implementation(AppDependencies.Compose.composeMaterial3)
    implementation(AppDependencies.Compose.composeNavigation)
    debugImplementation(AppDependencies.Compose.composeUiTooling)
}
fun DependencyHandler.Hilt() {
    implementation(AppDependencies.Hilt.hiltAndroid)
    kapt(AppDependencies.Hilt.androidxHiltCompiler)
    kapt(AppDependencies.Hilt.hiltAndroidCompiler)
}
fun DependencyHandler.Project(projectName: String) {
    implementation(project(projectName))
}
object AppDependencies {
    object Android {
        private const val coreKtx = "1.13.1"
        private const val appCompat = "1.7.0"
        private const val lifecycleRuntimeKtx = "2.8.4"
        const val androidxCooreKtx = "androidx.core:core-ktx:${coreKtx}"
        const val androidxAppcompat = "androidx.appcompat:appcompat:${appCompat}"
        const val androidxLifecycleRuntimeKtx = "androidx.lifecycle:lifecycle-runtime-ktx:${lifecycleRuntimeKtx}"
    }
    object Hilt {
        private const val hilt = "2.51.1"
        private const val hiltAndroidX = "1.2.0"
        const val hiltAndroid = "com.google.dagger:hilt-android:${hilt}"
        const val hiltAndroidCompiler = "com.google.dagger:hilt-android-compiler:${hilt}"
        const val androidxHiltCompiler = "androidx.hilt:hilt-compiler:${hiltAndroidX}"
    }
    object Compose {
        private const val composeBomVersion = "2024.08.00"
        private const val activityComposeVersion = "1.9.1"
        private const val composeNavigationVersion = "2.8.0"
        const val composeMaterial3 = "androidx.compose.material3:material3"
        const val composeUi = "androidx.compose.ui:ui"
        const val composeUiGraphics = "androidx.compose.ui:ui-graphics"
        const val composeUiTooling = "androidx.compose.ui:ui-tooling"
        const val composeUiToolingPreview = "androidx.compose.ui:ui-tooling-preview"
        const val composeBom = "androidx.compose:compose-bom:${composeBomVersion}"
        const val composeActivity = "androidx.activity:activity-compose:${activityComposeVersion}"
        const val composeNavigation = "androidx.navigation:navigation-compose:${composeNavigationVersion}"
    }
}В итоге gradle файл теперь выглядит так:
dependencies {
    Android()
    Compose()
    Hilt()
    Project(":mymodule")
}
Вывод
Такой подход позволяет комбинировать зависимости под нужды проекта и обладает рядом преимуществ:
- есть возможность управлять зависимостями(implementation, kapt, androidTestImplementation и тд) 
- сокращает количество кода 
- логика зависимостей инкапсулируется в функциях расширения 
- возможность переиспользвания 
- модули подключают зависимости только те зависимости, которые относятся к предметной области(конечно, если запретить подключать зависимости напрямую) 
- декларативный подход 
 
           
 

kavaynya
Помню как занимался таким до появления VersionCatalogs. Но сейчас используя зависимости в одном toml-файлике и conventions-плагины, могу получить такие gradle-конфиги:
А у него под капотом спрятана вся логика по настройке такого библиотечного модуля вместе с нужными зависимостями. И мне в конфиге остается только указать индивидуальные зависимости именно этого модуля.
Здесь зависимости прокидываются в модуль не явно, в отличии от вашего подхода. Но меня это полностью устраивает, так как всегда могу провалиться внутрь и вспромнить что там есть.
clint_eastwood Автор
про VersionCatalogs не слышал.
спасибо. гляну
kavaynya
Вы лукавите же? Вы разве не об этом вели речь в статье упоминая про toml-файл с зависимостями?
clint_eastwood Автор
нет. просто не правильно понял, наверное.
Spinoza0
Там ещё bundles есть...
clint_eastwood Автор
да есть. но bundles нет возможности установить способы подключения зависимости
где api, implementation, test ...
очередное "непоймичто" для хранения строк