Skip to main content

Command Palette

Search for a command to run...

[Android] Version Catalog + Convention Plugin으로 build.gradle 버전을 관리해보자!

Updated
4 min read
[Android] Version Catalog + Convention Plugin으로 build.gradle 버전을 관리해보자!

더 잘하고 싶은 안드로이드 개발자.

https://github.com/jinukeu

Why Version Catalog, Convention Plugin?

Multi Module의 버전을 관리하는 방법은 크게 세가지(ext, buildSrc, version catalog)가 있다. 각각의 장단점을 알고 싶다면 여기를 참고하면 된다. (대충 version catalog가 가장 좋다는 얘기)

Version Catalog 적용 순서

1. libs.versions.toml 파일 생성

gradle -> libs.versions.toml 파일을 추가한다.

내용은 이런 식으로 들어간다. 전체 코드는 여기에서 확인 가능하다.

코드를 살펴보면 [versions], [libraries], [bundles], [plugins] 키워드가 있다.

  • versions : 라이브러리 버전

  • libraries : 라이브러리 의존성

  • bundles : 라이브러리를 묶어서 한 번에 선언

  • plugins: 어떤 플러그인을 사용하는지

이제 버전 카탈로그를 사용하기 위한 준비는 끝났다.

2. 디펜던시 이전

Kotlin DSL로 작성된 Gradle에 Version Catalog를 적용해보자. (Gradle를 Groovy로 사용하고 있다면 Kotlin DSL로 이전하는 방법은 여기에서 확인할 수 있다.)

implementation("com.google.android.gms:play-services-auth:20.5.0")

예를 들어 위 디펜던시를 추가하고 싶다면 libs.versions.toml에 다음과 같이 입력하면 된다.

[versions]
play-services-auth = "20.5.0"

[libraries]
google-services-auth = { group = "com.google.android.gms", name = "play-services-auth", version.ref = "play-services-auth" }

libraries는 : 을 기준으로 group, name, version.ref으로 나뉜다. version.ref에는 [versions]에 적어놓은 이름을 적어주면 된다.

그 이후 Sync를 하고 build.gradle에 아래처럼(implementation(libs.google.services.auth)) 추가해주면 된다.

implementation(libs.google.services.auth) // 버전 카탈로그 적용 후

아까 적었던 -. 으로 대체할 수 있다. IDE 상에서 입력해보면, catalog 별로 자동완성을 지원함을 알 수 있다. 따라서 간편하게 추가가 가능하다.

3. 플러그인 이전

기존 플러그인이 아래와 같다면

// Top-level `build.gradle.kts` file
plugins {
   id("com.android.application") version "7.4.1" apply false
}

// Module-level `build.gradle.kts` file
plugins {
   id("com.android.application")
}

버전 카탈로그에 다음과 같이 적어주고 ...

[versions]
android-gradle-plugin = "7.4.1"

[plugins]
android-application = { id = "com.android.application", version.ref = "android-gradle-plugin" }

아래의 코드로 바꿔주면 된다.

// Top-level build.gradle.kts
plugins {
   alias(libs.plugins.android.application) apply false
}

// module build.gradle.kts
plugins {
   alias(libs.plugins.android.application)
}

4. Bundle 사용법

예를 들어 버전 카탈로그가 아래와 같다면 ...

[versions]
retrofit = "2.6.2"

[libraries]
# Network
retrofit-core = { module = "com.squareup.retrofit2:retrofit", version.ref = "retrofit" }
retrofit-adapter-rxjava = { module = "com.squareup.retrofit2:adapter-rxjava2", version.ref = "retrofit" }
retrofit-converter-gson = { module = "com.squareup.retrofit2:converter-gson", version.ref = "retrofit" }

[bundles]
retrofit = [
    "retrofit-core",
    "retrofit-adapter-rxjava",
    "retrofit-converter-gson"
]

번들을 사용하여 retrofit-core, retrofit-adapter-rxjava, retrofit-converter-gson 를 한번에 추가할 수 있다.

//build.gradle.kts

dependencies {
  implementation(libs.bundles.retrofit) // retrofit(core, rxjava, converter)
}

Convention Plugin 적용 순서

version catalog로 이전이 끝났다면 이제 Convention Plugin을 적용할 수 있다.

1. build-logic 모듈 생성

모듈 생성 후 안의 내용은 전부 비워주고 settings.gradle.kts 을 추가한다.

//settings.gradle.kts
dependencyResolutionManagement {
    repositories {
        google()
        mavenCentral()
    }
    versionCatalogs {
        create("libs") {
            from(files("../gradle/libs.versions.toml"))
        }
    }
}

rootProject.name = "build-logic"
include(":convention")

그리고 build-logic안에 convention이라는 모듈을 추가한다. (Java or Kotlin Library로 만들자.)

이런 패키지 구조를 가지면 된다.

그리고 :build-logic:convention의 build.gradle를 다음과 같이 수정한다.

@Suppress("DSL_SCOPE_VIOLATION") // TODO: Remove once KTIJ-19369 is fixed
plugins {
    `kotlin-dsl`
}

java {
    sourceCompatibility = JavaVersion.VERSION_17
    targetCompatibility = JavaVersion.VERSION_17
}

dependencies {
    compileOnly(libs.android.gradle.plugin)
    compileOnly(libs.kotlin.gradle.plugin)
    compileOnly(libs.ksp.gradle.plugin)
}

root 수준의 settings.gradle.kts는 이렇게 수정한다. (자동으로 추가된 include(":build-logic"), include(":build-logic:convention")를 지워줘야한다.)

pluginManagement {
    includeBuild("build-logic")
    repositories {
        google()
        mavenCentral()
        gradlePluginPortal()
    }
}

// ...
include(":app")
// include(":build-logic") // remove
// include(":build-logic:convention") // remove

이제 KotlinAndroid, Compose 세팅을 도와주는 확장 함수를 build-logic/convention 안에 추가한다.

// KotlinAndroid.kt
internal fun Project.configureKotlinAndroid(
    commonExtension: CommonExtension<*, *, *, *, *>,
) {
    commonExtension.apply {
        compileSdk = 34

        defaultConfig {
            minSdk = 26

            testInstrumentationRunner = "android.support.test.runner.AndroidJUnitRunner"
            vectorDrawables.useSupportLibrary = true
        }

        compileOptions {
            sourceCompatibility = JavaVersion.VERSION_17
            targetCompatibility = JavaVersion.VERSION_17
        }

        kotlinOptions {
            jvmTarget = JavaVersion.VERSION_17.toString()
        }
    }
}

fun CommonExtension<*, *, *, *, *>.kotlinOptions(block: KotlinJvmOptions.() -> Unit) {
    (this as ExtensionAware).extensions.configure("kotlinOptions", block)
}
// AndroidCompose.kt
internal fun Project.configureAndroidCompose(
    commonExtension: CommonExtension<*, *, *, *, *>,
) {
    val libs = extensions.getByType<VersionCatalogsExtension>().named("libs")

    commonExtension.apply {
        buildFeatures.compose = true

        composeOptions {
            kotlinCompilerExtensionVersion = libs.findVersion("compose.compiler").get().requiredVersion
        }
    }

    dependencies {
        // Disabling to work with Alpha
        "api"(platform(libs.findLibrary("compose.bom").get()))
        "implementation"(libs.findBundle("compose").get())
        "debugImplementation"(libs.findBundle("compose.debug").get())
    }
}

2. 컨벤션 플러그인 추가

convention에 app 모듈의 build.gradle 설정 플러그인을 추가한다.

internal class AndroidApplicationConventionPlugin : Plugin<Project> {

    override fun apply(target: Project) {
        with(target) {
            with(pluginManager) {
                apply("com.android.application")
                apply("org.jetbrains.kotlin.android")
            }

            extensions.configure<ApplicationExtension> {
                configureKotlinAndroid(this)
            }
        }
    }
}
class AndroidApplicationComposeConventionPlugin : Plugin<Project> {
    override fun apply(target: Project) {
        with(target) {
            pluginManager.apply("com.android.application")
            val extension = extensions.getByType<BaseAppModuleExtension>()
            configureAndroidCompose(extension)
        }
    }
}

이어서 다른 컨벤션들도 추가해준다. 코드는 여기서 확인할 수 있다.

프로젝트에 맞게 추가하면 된다. 나는 7개의 컨벤션을 만들었다.

이제 마지막으로 build-logic:convention 모듈의 build.gradle.kts 파일에 컨벤션을 등록해주면 된다.

@Suppress("DSL_SCOPE_VIOLATION") // TODO: Remove once KTIJ-19369 is fixed
plugins {
    `kotlin-dsl`
}

java {
    sourceCompatibility = JavaVersion.VERSION_17
    targetCompatibility = JavaVersion.VERSION_17
}

dependencies {
    compileOnly(libs.android.gradle.plugin)
    compileOnly(libs.kotlin.gradle.plugin)
    compileOnly(libs.ksp.gradle.plugin)
}

gradlePlugin {
    plugins {
        register("AndroidApplicationPlugin") {
            id = "done.plugin.application"
            implementationClass = "AndroidApplicationConventionPlugin"
        }
        register("AndroidApplicationComposePlugin") {
            id = "done.plugin.application.compose"
            implementationClass = "AndroidApplicationComposeConventionPlugin"
        }
        // ...
    }
}

id는 컨벤션 플러그인의 이름이고 (직접 지정하면 된다.) implementationClass는 id에 매칭되는 클래스 이름이다.

3. 적용해보기

예를 들어서 A 모듈에 Hilt를 추가하고 싶다면 귀찮은 설정 없이 A 모듈의 build.gradle.kts plugin에 id("buzzzzing.plugin.hilt") 한줄만 추가하면 된다.

plugins {
    id("done.plugin.hilt")
}

feature를 다수의 모듈로 관리하게 되면 build.gradle에 중복 코드가 많아지는데 위에서 언급한 FeatureConventionPlugin을 사용하면 쉽게 build.gradle를 관리할 수 있다.

블로그에 적어놓은 내용이 반영된 깃헙 주소를 아래 첨부한다.

https://github.com/Akatsuki-USW/Buzzzzing-Android

참고

https://brunch.co.kr/@purpledev/46

https://medium.com/prnd/%EB%9D%BC%EC%9D%B4%EB%B8%8C%EB%9F%AC%EB%A6%AC-%EB%B2%84%EC%A0%84%EA%B4%80%EB%A6%AC-%EB%8D%94%EC%9D%B4%EC%83%81-buildsrc%EB%A1%9C-%EC%82%AC%EC%9A%A9%ED%95%98%EC%A7%80-%EB%A7%88%EC%84%B8%EC%9A%94-feat-catalog-%ED%97%A4%EC%9D%B4%EB%94%9C%EB%9F%AC-%EA%B8%B0%EC%88%A0%EB%B8%94%EB%A1%9C%EA%B7%B8-710b4ca0870d

https://developer.android.com/build/migrate-to-catalogs#kts

https://medium.com/@kingjinho/android-gradle-version-catalog-%EC%A0%81%EC%9A%A9-85f41ae2ef6e

More from this blog

단위테스트 2장 정리

단위 테스트의 정의에는 많은 늬앙스가 있다. 크게 고전파, 런던파로 나뉜다. 고전파: 모든 사람이 단위 테스트와 테스트 주도 개발에 원론적으로 접근하는 방식 런던파: 런던의 프로그래밍 커뮤니티에서 시작 단위 테스트의 정의 단위 테스트는 작은 코드 조각을 검증하고 빠르게 수행하고 격리된 방식으로 처리하는 자동화된 테스트다. 격리 문제에 대한 런던파의 접근 코드 조각을 격리된 방식으로 검증한다는 것은 무엇을 의미할까? 런던파에서는 테스트 ...

Sep 3, 20243 min read

단위 테스트 1장 정리

단위 테스트 1장 정리 내용입니다. 단위 테스트에 시간을 투자할 때는 항상 최대한 이득을 얻도록 노력해야하며, 테스트에 드는 노력을 가능한 줄이고 그에 따르는 이득을 최대화해야 한다. 이 책에서 다루는 내용은 비용 편익 분석 방법을 배우고 특정 상황에서 적절한 테스트 기술을 적용할 수 있다. 또한 공통적인 안티 패턴을 피하는 방법도 배운다. 단위 테스트의 목표 단위 테스트의 목표는 무엇인가? 프로젝트의 지속 가능한 성장을 가능하게 하는 것이다...

Aug 27, 20242 min read

23년 하반기 회고

23년 상반기 회고에서 이어지는 글이다. 8 - 9월 이력서 제출 그리고 ... 이력서와 포트폴리오를 만들었다. 꽤나 잘 만들었다고 생각했다. 개발자 지인들의 첨삭, 인프런 멘토링, 유료 이력서 첨삭 서비스를 받으며 칭찬을 꽤 받았기 때문이다. 내가 보기에도 괜찮은 것 같고 ... 다른 사람들이 보기에도 괜찮다고 했으니 서류 합격률은 꽤 높을거라 생각했다. 50개의 서류를 넣은 결과, 3번의 서류 합격 그리고 단 한 곳에서만 최종합격했다. (최...

Jan 1, 20243 min read
23년 하반기 회고

[Android] Compose 수명 주기, 부수효과

수명 주기 개요 컴포지션은 UI를 기술하는 컴포저블의 트리 구조이다.컴포지션은 초기 컴포지션을 통해서만 생성되고 리컴포지션을 통해서만 업데이트 된다. 컴포저블의 수명 주기 컴포지션 시작 리컴포지션 컴포지션 종료 리컴포지션은 일반적으로 State<T> 객체가 변경되면 트리거됩니다. 컴포지션 내 컴포저블의 분석 컴포지션 내 컴포저블의 인스턴스는 호출 사이트(call site)로 식별된다. (호출 사이트는 컴포저블이 호출되는 소스코드 위치...

Dec 22, 20236 min read
[Android] Compose 수명 주기, 부수효과

[Android] rememberUpdatedState 완벽 이해

rememberUpdatedState 정의 공식 문서에는 다음과 같이 적혀있다. 값이 변경되는 경우 다시 시작되지 않아야 하는 효과(Effect)에서 값 참조 포스팅을 정리하면서 정의한 rememberUpdateState는 아래와 같다. remember는 초기 컴포지션에서만 값을 저장하고 리컴포지션 때 들어온 값은 저장하지 않는다. 리컴포지션 때 들어온 값도 저장하고 싶을 때 rememberUpdateState를 사용한다. 이게 도대체 ...

Dec 14, 20234 min read
[Android] rememberUpdatedState 완벽 이해

Jinukeu

33 posts