Введение
В многомодульных приложениях 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 ...
очередное "непоймичто" для хранения строк