-
Kotlin-DSL을 활용하여 Dependency 관리하기 - (2)안드로이드 2021. 5. 17. 02:19
이전 포스팅에서 Kotlin-DSL Gradle 설정하는 법 까지 진행 했다.
이를 활용해서 Dependency를 관리 하는법을 알아보자
Kotlin-DSL을 활용하여 Dependency 관리하기 - (1)
build.gradle.kts (:buildSrc)
gradle 파일 외부에서 kotlin 언어의 기능을 사용하기 위해서는 먼저 buildSrc라는 디렉토리를 만든다.
project 단에서 buildSrc라는 폴더를 만든 후 하위 디렉토리에 build.gradle.kts 파일을 만든 후 'sync now'를 누른다.
// project/buildSrc/build.gradle.kts plugins { `kotlin-dsl` } repositories { jcenter() }
plugins을 작성하지 않으면 다른 gradle에서 해당 디렉토리 하위에 있는 코드를 인식할 수 없다.
이후 buildSrc/src/main/java 디렉토리를 만들어주고 Version 과 Libs 파일을 만든다.
제대로 진행하고 있다면 위 이미지와 같은 구성이 된다.
Dependency를 Kotlin 코드로 관리 하기
internal object Versions { const val KOTLIN_STDLIB = "1.3.72" const val ANDROID_CORE_KTX = "1.3.2" const val ANDROID_APPCOMPAT = "1.2.0" const val ANDROID_MATERIAL = "1.3.0" const val ANDROID_CONSTRAINT = "2.0.4" const val TEST_JUNIT = "4.+" const val ANDROID_TEST_JUNIT = "1.1.2" const val ANDROID_TEST_ESPRESSO = "3.3.0" } object Libs { const val KOTLIN_STDLIB = "org.jetbrains.kotlin:kotlin-stdlib:${Versions.KOTLIN_STDLIB}" const val ANDROID_CORE_KTX = "androidx.core:core-ktx:${Versions.ANDROID_CORE_KTX}" const val ANDROID_APPCOMPAT = "androidx.appcompat:appcompat:${Versions.ANDROID_APPCOMPAT}" const val ANDROID_MATERIAL = "com.google.android.material:material:${Versions.ANDROID_MATERIAL}" const val ANDROID_CONSTRAINT = "androidx.constraintlayout:constraintlayout:${Versions.ANDROID_CONSTRAINT}" const val TEST_JUNIT = "junit:junit:${Versions.TEST_JUNIT}" const val ANDROID_TEST_JUNIT = "androidx.test.ext:junit:${Versions.ANDROID_TEST_JUNIT}" const val ANDROID_TEST_ESPRESSO = "androidx.test.espresso:espresso-core:${Versions.ANDROID_TEST_ESPRESSO}" }
plugins { id("com.android.application") id("kotlin-android") } android { compileSdkVersion(29) buildToolsVersion("29.0.3") defaultConfig { applicationId = "com.munny.kotlindslsample" minSdkVersion(23) targetSdkVersion(29) versionCode(1) versionName("1.0") testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" } buildTypes { getByName("release") { minifyEnabled(false) proguardFiles( getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro" ) } } compileOptions { sourceCompatibility = JavaVersion.VERSION_1_8 targetCompatibility = JavaVersion.VERSION_1_8 } kotlinOptions { jvmTarget = JavaVersion.VERSION_1_8.toString() } } dependencies { implementation(Libs.KOTLIN_STDLIB) implementation(Libs.ANDROID_CORE_KTX) implementation(Libs.ANDROID_APPCOMPAT) implementation(Libs.ANDROID_MATERIAL) implementation(Libs.ANDROID_CONSTRAINT) testImplementation(Libs.TEST_JUNIT) androidTestImplementation(Libs.ANDROID_TEST_JUNIT) androidTestImplementation(Libs.ANDROID_TEST_ESPRESSO) }
상수로 작성된 dependency들을 kotlin 코드로 옮겼다.
이렇게 작성된 코드를 보고 이런 생각을 가졌다.
똑같은 implementation이 반복되고 있으니 반복문으로 처리할 수 있지 않을까?
그래서 비슷한 성격을 가진 Dependency끼리 묶어주기로 했다.
라이브러리 단위로 클래스 묶기
interface Libs { fun getDependencies(): List<String> object Kotlin : Libs { private const val STDLIB = "org.jetbrains.kotlin:kotlin-stdlib:${Versions.KOTLIN_STDLIB}" override fun getDependencies() = listOf( STDLIB ) } object Android : Libs { private const val CORE_KTX = "androidx.core:core-ktx:${Versions.ANDROID_CORE_KTX}" private const val APPCOMPAT = "androidx.appcompat:appcompat:${Versions.ANDROID_APPCOMPAT}" private const val MATERIAL = "com.google.android.material:material:${Versions.ANDROID_MATERIAL}" private const val CONSTRAINT = "androidx.constraintlayout:constraintlayout:${Versions.ANDROID_CONSTRAINT}" override fun getDependencies() = listOf( CORE_KTX, APPCOMPAT, MATERIAL, CONSTRAINT ) } object Test : Libs { private const val JUNIT = "junit:junit:${Versions.TEST_JUNIT}" override fun getDependencies() = listOf( JUNIT ) } object AndroidTest : Libs { private const val JUNIT = "androidx.test.ext:junit:${Versions.ANDROID_TEST_JUNIT}" private const val ESPRESSO = "androidx.test.espresso:espresso-core:${Versions.ANDROID_TEST_ESPRESSO}" override fun getDependencies() = listOf( JUNIT, ESPRESSO ) } }
// build.gradle.kts (:app) enum class ImplementationType { IMPLEMENTATION, TEST_IMPLEMENTATION, ANDROID_TEST_IMPLEMENTATION } dependencies { implementationDependencies(ImplementationType.IMPLEMENTATION, Libs.Kotlin) implementationDependencies(ImplementationType.IMPLEMENTATION, Libs.Android) implementationDependencies(ImplementationType.TEST_IMPLEMENTATION, Libs.Test) implementationDependencies(ImplementationType.ANDROID_TEST_IMPLEMENTATION, Libs.AndroidTest) } fun DependencyHandler.implementationDependencies(type: ImplementationType, libs: Libs) { libs.getDependencies().forEach { when (type) { ImplementationType.IMPLEMENTATION -> implementation(it) ImplementationType.TEST_IMPLEMENTATION -> testImplementation(it) ImplementationType.ANDROID_TEST_IMPLEMENTATION -> androidTestImplementation(it) } } }
비슷한 유형에 속하는 라이브러리들을 클래스 단위로 묶고 getDependencies() 함수를 가진
Libs 인터페이스를 상속받는 구조로 바꿨다.
이런식으로 관리하면 코드가 간결해지지만 아직 의문이 남아있다.
"androidx.room:room-runtime:${Versions.ROOM}" "androidx.room:room-ktx:${Versions.ROOM}" "androidx.room:room-compiler:${Versions.ROOM}"
예시로 Room을 사용 할 때의 포함하는 Dependency를 가져왔다.
위 2개의 Dependency는 implementation으로 포함시켜야 하는데 아래의 compiler는 kapt로 포함 시켜야한다.
이 때 Room 단위로 클래스를 묶게되면
object Room : Libs { private const val CORE = "androidx.room:room-runtime:${Versions.ROOM}" private const val KTX = "androidx.room:room-ktx:${Versions.ROOM}" private const val COMPILER = "androidx.room:room-compiler:${Versions.ROOM}" override fun getDependencies() = listOf( CORE, KTX, COMPILER ) }
이렇게 되는데 아까 만든 함수는 여러 종류의 Dependency를 포함할 수 없으므로
코드를 추가로 작성하기로 했다.
sealed class를 활용해서 Dependency와 종류를 묶어내기
sealed class DependencyType(val value: Any) { data class Implementation(private val notation: Any) : DependencyType(notation) data class TestImplementation(private val notation: Any) : DependencyType(notation) data class AndroidTestImplementation(private val notation: Any) : DependencyType(notation) data class Kapt(private val notation: Any) : DependencyType(notation) } interface Libs { fun getDependencies(): List<DependencyType> object Kotlin : Libs { private const val STDLIB = "org.jetbrains.kotlin:kotlin-stdlib:${Versions.KOTLIN_STDLIB}" override fun getDependencies() = listOf( DependencyType.Implementation(STDLIB) ) } object Android : Libs { private const val CORE_KTX = "androidx.core:core-ktx:${Versions.ANDROID_CORE_KTX}" private const val APPCOMPAT = "androidx.appcompat:appcompat:${Versions.ANDROID_APPCOMPAT}" private const val MATERIAL = "com.google.android.material:material:${Versions.ANDROID_MATERIAL}" private const val CONSTRAINT = "androidx.constraintlayout:constraintlayout:${Versions.ANDROID_CONSTRAINT}" override fun getDependencies() = listOf( DependencyType.Implementation(CORE_KTX), DependencyType.Implementation(APPCOMPAT), DependencyType.Implementation(MATERIAL), DependencyType.Implementation(CONSTRAINT) ) } object Room : Libs { private const val CORE = "androidx.room:room-runtime:${Versions.ROOM}" private const val KTX = "androidx.room:room-ktx:${Versions.ROOM}" private const val COMPILER = "androidx.room:room-compiler:${Versions.ROOM}" override fun getDependencies() = listOf( DependencyType.Implementation(CORE), DependencyType.Implementation(KTX), DependencyType.Kapt(COMPILER) ) } object JUnit : Libs { private const val JUNIT = "junit:junit:${Versions.TEST_JUNIT}" private const val ANDROID_JUNIT = "androidx.test.ext:junit:${Versions.ANDROID_TEST_JUNIT}" override fun getDependencies() = listOf( DependencyType.TestImplementation(JUNIT), DependencyType.AndroidTestImplementation(ANDROID_JUNIT) ) } object AndroidTest : Libs { private const val ESPRESSO = "androidx.test.espresso:espresso-core:${Versions.ANDROID_TEST_ESPRESSO}" override fun getDependencies() = listOf( DependencyType.AndroidTestImplementation(ESPRESSO) ) } }
// build.gradle.kts (:app) dependencies { implementationDependencies(Libs.Kotlin) implementationDependencies(Libs.Android) implementationDependencies(Libs.Room) implementationDependencies(Libs.JUnit) implementationDependencies(Libs.AndroidTest) } fun DependencyHandler.implementationDependencies(libs: Libs) { libs.getDependencies().forEach { val notation = it.value when (it) { is DependencyType.Implementation -> implementation(notation) is DependencyType.TestImplementation -> testImplementation(notation) is DependencyType.AndroidTestImplementation -> androidTestImplementation(notation) is DependencyType.Kapt -> kapt(notation) } } }
이렇게 작성된 코드에서는 app모듈의 gradle에서는 해당 라이브러리가 어떠한 Dependency인지 종류인지
알 필요 없이 간단하게 포함할 수 있게 됐다.
implementationDependencies 함수를 전역적으로 사용하기
이렇게 작성하게 되면 어떤 이점이 있는가 의문이 들 수 있는데
라이브러리 단위로 Dependency를 관리하면 멀티 모듈 환경에서 손쉽게 동일한
라이브러리를 추가할 수 있는 장점이 있다.
그런데 현재 implementationDependencies 함수는 app 모듈에 종속되어 있으므로 buildSrc로 옮겼다.
하지만 간단하게 옮겨질거란 예상과는 달리 함수들을 찾을 수 없다고 나온다.
아마도 내부 구현이 전부 extension 형태로 되어있어서 buildSrc에서 접근 할 수 없는 것 같다.
그런데 자세히 보면 공통적으로 DependencyHandler.add(함수명, Dependency) 형태를 호출하게
되어있다.
그래서 같은 형태로 호출하게 작성했다.
// buildSrc/src/main/java/DependencyHandlerExtension.kt fun DependencyHandler.implementationDependencies(libs: Libs) { libs.getDependencies().forEach { dependencyType -> val typeName = dependencyType.javaClass.simpleName.run { replace(first(), first().toLowerCase()) } add(typeName, dependencyType.value) } }
// build.gradle.kts (:app) dependencies { implementationDependencies(Libs.Kotlin) implementationDependencies(Libs.Android) implementationDependencies(Libs.Room) implementationDependencies(Libs.JUnit) implementationDependencies(Libs.AndroidTest) }
아까 만든 sealed class의 하위목록의 첫글자를 소문자로 변환해서 add의 인자에 넣었다.
이제 새로 만드는 모든 module에서 implementationDependencies 함수를 이용해서 쉽게 의존성을 추가할 수 있게 됐다.
코드가 많아서 포스팅이 생각보다 길어졌지만 과정의 코드를
기록해두고 싶은 마음에 결과물에 비해 길게 써져버렸다.
마지막 문단의 코드는 아무래도 상수가 들어가는 코드이기 때문에 100% 안전한 코드라고는 보장할 수 없어서
안전하게 작성하려면 이전 문단의 함수를 모듈마다 작성하면 될것같다.
예시로 작성한 코드는 제 깃허브 링크에서 확인 할 수 있습니다.
반응형'안드로이드' 카테고리의 다른 글
[Android/Retrofit] 204 응답에 NPE 발생할 때 대처법 (0) 2024.02.04 [Android/WebView] 안드로이드에서 WebView의 함수 호출하기 (0) 2022.01.06 [Compose] Material ImageVector로 Icon 사용하기 (0) 2021.12.27 Kotlin-DSL을 활용하여 Dependency 관리하기 - (1) (0) 2021.05.16 [Android/Kotlin] Multi-ViewType을 사용하는 RecyclerView의 구조를 추상화 해보기 (0) 2021.04.04