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

от автора

Введение

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

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

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

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

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

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


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


Комментарии

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

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