Введение

В многомодульных приложениях 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 и тд)

  • сокращает количество кода

  • логика зависимостей инкапсулируется в функциях расширения

  • возможность переиспользвания

  • модули подключают зависимости только те зависимости, которые относятся к предметной области(конечно, если запретить подключать зависимости напрямую)

  • декларативный подход

Комментарии (8)


  1. kavaynya
    25.09.2024 06:35
    +2

    Помню как занимался таким до появления VersionCatalogs. Но сейчас используя зависимости в одном toml-файлике и conventions-плагины, могу получить такие gradle-конфиги:

    конфиг  feature-модуля
    конфиг feature-модуля

    А у него под капотом спрятана вся логика по настройке такого библиотечного модуля вместе с нужными зависимостями. И мне в конфиге остается только указать индивидуальные зависимости именно этого модуля.
    Здесь зависимости прокидываются в модуль не явно, в отличии от вашего подхода. Но меня это полностью устраивает, так как всегда могу провалиться внутрь и вспромнить что там есть.


    1. clint_eastwood Автор
      25.09.2024 06:35

      про VersionCatalogs не слышал.

      спасибо. гляну


      1. kavaynya
        25.09.2024 06:35

        Вы лукавите же? Вы разве не об этом вели речь в статье упоминая про toml-файл с зависимостями?


        1. clint_eastwood Автор
          25.09.2024 06:35

          нет. просто не правильно понял, наверное.


      1. Spinoza0
        25.09.2024 06:35

        Там ещё bundles есть...


        1. clint_eastwood Автор
          25.09.2024 06:35

          да есть. но bundles нет возможности установить способы подключения зависимости
          где api, implementation, test ...
          очередное "непоймичто" для хранения строк


  1. Dertefter
    25.09.2024 06:35

    Жесть какая-то


    1. clint_eastwood Автор
      25.09.2024 06:35

      что именно есть жесть?