Объектно ориентированный подход в организации gradle зависимостей в Android проектах

c466fdd63173c146baf6eb9687c797c5.jpg

Введение

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

Теперь создадим объекты зависимостей

object Android {
    operator fun DependencyHandler.invoke() {
        implementation(AppDependencies.Android.androidxCooreKtx)
        implementation(AppDependencies.Android.androidxAppcompat)
        implementation(AppDependencies.Android.androidxLifecycleRuntimeKtx)
    }
}

object Compose {
    operator fun DependencyHandler.invoke() {
        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)
    }
}

object Hilt {
    operator fun DependencyHandler.invoke() {
        implementation(AppDependencies.Hilt.hiltAndroid)
        kapt(AppDependencies.Hilt.androidxHiltCompiler)
        kapt(AppDependencies.Hilt.hiltAndroidCompiler)
    }
}

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")
}

42c61be748ef0448d944bf02114651b6.jpg

Вывод

Такой подход позволяет комбинировать зависимости под нужды проекта и обладает рядом преимуществ:

  • есть возможность управлять зависимостями (implementation, kapt, androidTestImplementation и тд)

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

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

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

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

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

© Habrahabr.ru