본문 바로가기

안드로이드/멀티 모듈

[안드로이드 멀티 모듈] 4. AndroidApplicationConventionPlugin 만들기


모듈을 만들었을 때 생성되는 build.gradle.kts 파일의 빌드 관련 중복 코드를 제거하고, 한 곳에서 관리하기 위한 작업을 진행 중입니다. 그래서 지난 포스팅에서는 build-logic:convention 모듈을 만들었습니다. 이번 포스팅에서는 AndroidApplicationConventionPlugin을 만들고 build.gradle.kts(Module :app) 모듈에 적용하겠습니다.

이번 포스팅에서 생성한 파일 및 디렉토리 구조
이번 포스팅에서 생성한 파일 및 디렉토리 구조

1. AndroidApplicationConventionPlugin.kt 만들기

build-logic모듈의 java 패키지 폴더에 AndroidApplicationConventionPlugin.kt을 만듭니다.

// AndroidApplicationConventionPlugin.kt
import org.gradle.api.Plugin
import org.gradle.api.Project

class AndroidApplicationConventionPlugin: Plugin<Project> {
    override fun apply(target: Project) {
        TODO("Not yet implemented")
    }
}


컨벤션 플러그인을 만들기 위해서는 반드시 org.gradle.api.Plugin 인터페이스를 구현해야 합니다.
그 후, apply 함수에 빌드 관련 코드를 넣어야 합니다.
플러그인을 명시적으로 적용하면 apply 함수가 호출되기 때문입니다.

2. AndroidApplicationConventionPlugin.kt 빌드 코드 작성하기

AndroidApplicationConventionPlugin을 만들었으니 빌드에 관련된 코드를 컨벤션 플러그인에 작성해야 합니다. 계속 말씀드리지만 컨벤션 플러그인을 만들면 여러 가지 모듈에서 해당 컨벤션 플러그인만 적용하면 중복되는 코드 없이 빌드를 진행할 수 있게 됩니다.

먼저, 컨벤션 플러그인의 Project 블록 안에서 version catalog를 편하게 사용하기 위해서 확장함수를 만들어줍니다.

// ProjectExt.kt
package com.multi.module.convention

import org.gradle.api.Project
import org.gradle.api.artifacts.VersionCatalog
import org.gradle.api.artifacts.VersionCatalogsExtension
import org.gradle.kotlin.dsl.getByType

val Project.libs: VersionCatalog
    get() = extensions.getByType<VersionCatalogsExtension>().named("libs")


그리고 아래와 같이 AndroidApplicationConventionPlugin의 apply 함수를 구현합니다.
configureKotlinAndroid는 유틸 함수로 만들어서 다른 컨벤션 플러그인에서도 공통으로 사용할 수 있게 구성했습니다.

코드를 보면 기존의 build.gradle.kts에 작성된 내용들이 약간은 다른 형태로 구성돼 있는 것을 볼 수 있습니다.

// AndroidApplicationConventionPlugin.kt
import com.android.build.api.dsl.ApplicationExtension
import com.multi.module.convention.configureKotlinAndroid
import com.multi.module.convention.libs
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.kotlin.dsl.configure

class AndroidApplicationConventionPlugin: Plugin<Project> {
    override fun apply(target: Project) {
        target.run {
            pluginManager.run {
                apply("com.android.application")
                apply("org.jetbrains.kotlin.android")
            }

            extensions.configure<ApplicationExtension> {
                defaultConfig {
                    applicationId = libs.findVersion("projectApplicationId").get().toString()
                    targetSdk = libs.findVersion("projectTargetSdkVersion").get().toString().toInt()
                    versionCode = libs.findVersion("projectVersionCode").get().toString().toInt()
                    versionName = libs.findVersion("projectVersionName").get().toString()
                }

                configureKotlinAndroid(this)
            }
        }
    }
}
// libs.versions.toml
[versions]
# Project versions
projectApplicationId = "com.multi.module.template"
projectVersionName = "1.0"
projectVersionCode = "1"
projectMinSdkVersion = "24"
projectTargetSdkVersion = "34"
projectCompileSdkVersion = "34"
...


아래는 configureKotlinAndroid와 configureKotlin 유틸 함수입니다.
유틸 함수로 만든 이유는 다른 컨벤션 플러그인에도 공통으로 들어갈 코드이기 때문입니다.

// Kotlin.kt
package com.multi.module.convention

import com.android.build.api.dsl.CommonExtension
import org.gradle.api.JavaVersion
import org.gradle.api.Project
import org.gradle.kotlin.dsl.dependencies
import org.gradle.kotlin.dsl.withType
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile

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

        defaultConfig.minSdk = 24

        compileOptions {
            isCoreLibraryDesugaringEnabled = true
            sourceCompatibility = JavaVersion.VERSION_11
            targetCompatibility = JavaVersion.VERSION_11
        }
    }

    configureKotlin()

    dependencies {
        "coreLibraryDesugaring"(libs.findLibrary("desugar.jdk.libs").get())
    }
}

private fun Project.configureKotlin() {
    tasks.withType<KotlinCompile>().configureEach {
        kotlinOptions {
            jvmTarget = JavaVersion.VERSION_11.toString()
        }
    }
}
// libs.versions.toml
[versions]
...
desugar_jdk_libs = "2.0.4"

[libraries]
...
desugar-jdk-libs = { module = "com.android.tools:desugar_jdk_libs", version.ref = "desugar_jdk_libs" }


Java 11을 사용하는 API 중에서 안드로이드 API 최소 기준이 높은 경우가 있는데, Desugaring은 26 버전이 필요한 API를 21의 버전에서도 사용할 수 있게해 줍니다. 대표적으로는 LocalDateTIme, LocalDate, ZondedDateTime과 같은 유틸리티 함수들이 있습니다.

이렇게 AndroidApplicationConventionPlugin을 만들었는데 어떻게 적용할 수 있을까요?

3. AndroidApplicationConventionPlugin 적용하기

먼저, Gradle에게 특정 id로 컨벤션 플러그인을 인식할 수 있도록 알려야 합니다.
libs.versions.toml에 id를 정의하겠습니다.

// libs.versions.toml
...

[plugins]
...

# Custom Convention Plugin
multi-module-android-application = { id = "multi.module.android.application", version = "unspecified"}


그리고 build-logic 모듈에 모든 컨벤션 플러그인을 등록해야 합니다. 아래처럼 build.gradle.kts (:build-logic:convention) 파일의 하단에 컨벤션 플러그인을 등록해 줍니다. 이때 입력하는 id는 libs.versions.toml 파일에 작성한 id와 반드시 동일해야 합니다.

// build.gradle.kts (Module :build-logic:convention)
...

gradlePlugin {
   plugins {
      register("androidApplication") {
         id = "multi.module.android.application"
         implementationClass = "AndroidApplicationConventionPlugin"
      }
   }
}


마지막으로 build.gradle.kts (:app) 모듈에 컨벤션 플러그인을 적용하면 됩니다.

// build.gradle.kts (:app)
plugins {
    // 컨벤션 플러그인 적용
    alias(libs.plugins.multi.module.android.application)
}

android {
    namespace = "com.multi.module.template"

    defaultConfig {
        testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
        vectorDrawables {
            useSupportLibrary = true
        }
    }

    // 다른 컨벤션 플러그인을 만들면서 공통으로 관리될 예정
    buildTypes {
        release {
            isMinifyEnabled = false
            proguardFiles(
                getDefaultProguardFile("proguard-android-optimize.txt"),
                "proguard-rules.pro"
            )
        }
    }

    // 다른 컨벤션 플러그인을 만들면서 공통으로 관리될 예정
    buildFeatures {
        compose = true
    }
    
    // 다른 컨벤션 플러그인을 만들면서 공통으로 관리될 예정
    composeOptions {
        kotlinCompilerExtensionVersion = "1.5.1"
    }
    
    packaging {
        resources {
            excludes += "/META-INF/{AL2.0,LGPL2.1}"
        }
    }
}

dependencies {
    ...
}


코드를 살펴보면 applicationId, targetSdk, versionCode, versionName, compileOptions, kotlinOptions 등이 제거된 것을 확인할 수 있습니다. 해당 내용은 컨벤션 플러그인에 적용했기 때문입니다.

4. 정리

이번 포스팅에서는 AndroidApplicationConventionPlugin을 만들고 적용했습니다.

XML 기반의 프로젝트인 경우, 해당 플러그인을 app 모듈에 적용할 수 있지만,
Compose 기반의 프로젝트라면 AndroidApplicationComposeConventionPlugin을 만들어야합니다.

그리고 모듈에서 공통으로 사용되는  buildType, buildFeature, composeOptions 관련 코드가 남아있는데, 이어지는 포스팅에서 다른 컨벤션 플러그인을 만들면서 제거할 예정입니다.

멀티 모듈 아키텍처를 구성하면 모듈을 만들 때마다 build.gradle.kts이 생기므로, 컨벤션 플러그인을 만들고 적용해 주는 테크닉이 필요합니다. 이렇게 하지 않으면 수많은 중복 코드가 발생할 수 있기 때문입니다.

이번 포스팅의 결과물은 아래 Github Repository의 4-AndroidApplicationConventionPlugin 브랜치를 확인하시면 됩니다.

 

GitHub - taein8935/multi-module-template-aos

Contribute to taein8935/multi-module-template-aos development by creating an account on GitHub.

github.com