
프로젝트에 build-logic을 사용해 커스텀 플러그인을 적용해보자.

먼저 build-logic 디렉토리를 만들어준다.
반드시 프로젝트 내부에 있을 필요는 없지만 Android Studio에서 보기 편하기에 프로젝트 내에 모듈로 생성하였다.

모듈을 만들면 빌드 스크립트만 생성되고 settings.gradle.kts는 자동생성해주지 않는다.
build-logic을 독립적인 빌드로 관리하기 위해서는 settings.gradle.kts를 가지고 있어야한다.
dependencyResolutionManagement {
repositories {
google()
mavenCentral()
}
versionCatalogs {
// 여기서 생성된 libs는
// extensions.getByType<VersionCatalogsExtension>().named("libs") 로
// 불러올 수 있다.
create("libs") {
from(files("../gradle/libs.versions.toml"))
}
}
}
rootProject.name = "build-logic" // 루트 프로젝트의 이름. 생성한 디렉토리와 같아야한다.
include(":convention") // 커스텀 플러그인 등 공용 로직을 정의할 모듈 이름
build-logic을 구성해주었다면, 빌드 로직을 사용할 프로젝트에 해당 빌드를 포함시켜주어야한다.
pluginManagement {
includeBuild("build-logic") // build-logic 를 포함시켜준다
...
}
...
build-logic을 사용할 루트 프로젝트의 settings.gradle.kts에 포함시켜주어야 Gradle이 정상적으로 인식하여 빌드를 포함시킨다.
build-logic 안에 Gradle API를 활용한 플러그인을 정의해 매번 의존성에 대한 정의를 할 필요 없이 해당 코드를 재사용할 수 있다.
컴포즈와 관련된 커스텀 플러그인을 만들것이기 때문에 필요한 라이브러리를 추가해주어야한다.
build-logic과 프로젝트 모두에서 같은 라이브러리를 사용할 것이기 때문에 프로젝트에 존재하는 버전 카탈로그에 필요한 라이브러리를 추가한다.
[versions]
# gradle
agp = "8.7.1"
kotlin = "2.0.0"
# ksp
ksp = "2.0.20-1.0.25"
# android core
coreKtx = "1.15.0"
junit = "4.13.2"
junitVersion = "1.2.1"
espressoCore = "3.6.1"
# kotlinx
kotlinxSerializationCore = "1.5.1"
kotlinxImmutableCollections = "0.3.5"
# compose
composeBom = "2024.04.01"
lifecycleRuntimeKtx = "2.8.7"
activityCompose = "1.9.3"
viewmodel-compose = "2.8.6"
lifecycleRuntimeComposeAndroid = "2.8.6"
navigation-compose = "2.8.3"
# hilt
hilt = "2.50"
hilt-navigation-compose = "1.2.0"
[libraries]
# gradle
android-gradlePlugin = { group = "com.android.tools.build", name = "gradle", version.ref = "agp" }
kotlin-gradlePlugin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" }
# android core
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
junit = { group = "junit", name = "junit", version.ref = "junit" }
androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" }
# kotlinx
kotlinx-serialization-core = { module = "org.jetbrains.kotlinx:kotlinx-serialization-core", version.ref = "kotlinxSerializationCore" }
kotlinx-collections-immutable = { module = "org.jetbrains.kotlinx:kotlinx-collections-immutable", version.ref = "kotlinxImmutableCollections" }
# android compose
androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" }
androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycleRuntimeKtx" }
androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" }
androidx-ui = { group = "androidx.compose.ui", name = "ui" }
androidx-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics" }
androidx-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" }
androidx-material3 = { group = "androidx.compose.material3", name = "material3" }
androidx-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" }
androidx-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" }
androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" }
androidx-lifecycle-viewmodel = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-compose", version.ref = "viewmodel-compose" }
androidx-lifecycle-runtime-compose-android = { group = "androidx.lifecycle", name = "lifecycle-runtime-compose-android", version.ref = "lifecycleRuntimeComposeAndroid" }
androidx-navigation-compose = { group = "androidx.navigation", name = "navigation-compose", version.ref = "navigation-compose" }
# hilt
hilt-compiler = { group = "com.google.dagger", name = "hilt-compiler", version.ref = "hilt" }
hilt-android = { group = "com.google.dagger", name = "hilt-android", version.ref = "hilt" }
hilt-android-testing = { group = "com.google.dagger", name = "hilt-android-testing", version.ref = "hilt" }
hilt-core = { group = "com.google.dagger", name = "hilt-core", version.ref = "hilt" }
hilt-navigation-compose = { group = "androidx.hilt", name = "hilt-navigation-compose", version.ref = "hilt-navigation-compose" }
[bundles]
android-core = [ "androidx-core-ktx", "junit", "androidx-junit", "androidx-espresso-core" ]
[plugins]
android-library = { id = "com.android.library", version.ref = "agp" }
android-application = { id = "com.android.application", version.ref = "agp" }
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }
hilt = { id = "com.google.dagger.hilt.android", version.ref = "hilt" }
# custom plugins
module-android-application = { id = "skele.android.application", version = "unspecified" }
module-android-library = { id = "skele.android.library", version = "unspecified" }
module-android-application-compose = { id = "skele.android.application.compose", version = "unspecified" }
module-android-library-compose = { id = "skele.android.library.compose", version = "unspecified" }
module-hilt = { id = "skele.android.hilt", version = "unspecified" }
커스텀 플러그인을 만들기에 앞서서 build-logic의 빌드 스크립트에 필요한 설정을 해주어야한다.
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
plugins {
`kotlin-dsl` // Kotlin 으로 커스텀 플러그인을 만들 수 있게 해준다.
}
java {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
kotlin {
compilerOptions {
jvmTarget = JvmTarget.JVM_17
}
}
dependencies {
compileOnly(libs.android.gradlePlugin) // 안드로이드 Gradle 플러그인
compileOnly(libs.kotlin.gradlePlugin) // 코틀린 Gradle 플러그인
}
빌드 스크립트의 설정이 끝났다면, Kotlin으로 플러그인을 작성하면 된다.
안드로이드 개발팀의 nowinandroid를 참고하여 작성하였다.
간편하게 버전 카탈로그를 가져오기 위한 Extension Property를 정의한다.
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
get(): VersionCatalog = extensions.getByType<VersionCatalogsExtension>().named("libs")
안드로이드에서 같은 버전을 사용하고 관리하기 위한 Extension Function.
import com.android.build.api.dsl.CommonExtension
import org.gradle.api.JavaVersion
import org.gradle.api.Project
import org.gradle.kotlin.dsl.assign
import org.gradle.kotlin.dsl.configure
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
import org.jetbrains.kotlin.gradle.dsl.KotlinAndroidProjectExtension
internal fun Project.configureKotlinAndroid(
commonExtension: CommonExtension<*, *, *, *, *, *>,
) {
commonExtension.apply {
defaultConfig.minSdk = 26
compileSdk = 35
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
}
configure<KotlinAndroidProjectExtension>{
compilerOptions.jvmTarget = JvmTarget.JVM_17
}
}
컴포즈에 필요한 다양한 의존성을 정의한다.
import com.android.build.api.dsl.CommonExtension
import org.gradle.api.Project
import org.gradle.kotlin.dsl.apply
import org.gradle.kotlin.dsl.dependencies
internal fun Project.configureAndroidCompose(commonExtension: CommonExtension<*, *, *, *, *, *>) {
commonExtension.apply {
apply(plugin = "org.jetbrains.kotlin.plugin.compose")
apply(plugin = "org.jetbrains.kotlin.plugin.serialization")
buildFeatures {
compose = true
}
composeOptions {
kotlinCompilerExtensionVersion = "1.5.1"
}
dependencies {
val bom = libs.findLibrary("androidx.compose.bom").get()
add("implementation", platform(bom))
add("androidTestImplementation", platform(bom))
add("implementation", libs.findLibrary("androidx.lifecycle.runtime.ktx").get())
add("implementation", libs.findLibrary("androidx.activity.compose").get())
add("implementation", libs.findLibrary("androidx.ui").get())
add("implementation", libs.findLibrary("androidx.ui-graphics").get())
add("implementation", libs.findLibrary("androidx.ui.tooling.preview").get())
add("implementation", libs.findLibrary("androidx.material3").get())
add("debugImplementation", libs.findLibrary("androidx.ui.tooling").get())
add("debugImplementation", libs.findLibrary("androidx.ui.test.manifest").get())
add(
"androidTestImplementation",
libs.findLibrary("androidx.ui.test.junit4").get(),
)
add("implementation", libs.findLibrary("androidx.lifecycle.viewmodel").get())
add(
"implementation",
libs.findLibrary("androidx.lifecycle.runtime.compose.android").get(),
)
// compose navigation
add("implementation", libs.findLibrary("kotlinx.serialization.core").get())
add("implementation", libs.findLibrary("androidx.navigation.compose").get())
// compose material
add("implementation", libs.findLibrary("androidx.material3").get())
// compose immutable
add("implementation", libs.findLibrary("kotlinx.collections.immutable").get())
}
}
}
import com.android.build.api.dsl.ApplicationExtension
import com.skele.build_logic.convention.extension.configureAndroidCompose
import com.skele.build_logic.convention.extension.configureKotlinAndroid
import com.skele.build_logic.convention.extension.libs
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.kotlin.dsl.apply
import org.gradle.kotlin.dsl.configure
import org.gradle.kotlin.dsl.dependencies
class AndroidApplicationComposeConventionPlugin : Plugin<Project> {
override fun apply(target: Project) {
with(target) {
apply(plugin = "com.android.application")
apply(plugin = "org.jetbrains.kotlin.android")
configure<ApplicationExtension>{
configureKotlinAndroid(this)
defaultConfig.targetSdk = 34
configureAndroidCompose(this)
}
dependencies {
add("implementation", libs.findBundle("android.core").get())
}
}
}
}
import com.android.build.api.dsl.LibraryExtension
import com.skele.build_logic.convention.extension.configureAndroidCompose
import com.skele.build_logic.convention.extension.configureKotlinAndroid
import com.skele.build_logic.convention.extension.libs
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.kotlin.dsl.apply
import org.gradle.kotlin.dsl.configure
import org.gradle.kotlin.dsl.dependencies
class AndroidLibraryComposeConventionPlugin : Plugin<Project> {
override fun apply(target: Project) {
with(target) {
apply(plugin = "com.android.library")
apply(plugin = "org.jetbrains.kotlin.android")
configure<LibraryExtension>{
configureKotlinAndroid(this)
lint.targetSdk = 34
configureAndroidCompose(this)
}
dependencies {
add("implementation", libs.findBundle("android.core").get())
}
}
}
}
import com.skele.build_logic.convention.extension.libs
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.kotlin.dsl.dependencies
class HiltConventionPlugin : Plugin<Project> {
override fun apply(target: Project) {
with(target) {
with(pluginManager){
apply(libs.findPlugin("ksp").get().get().pluginId)
apply(libs.findPlugin("hilt").get().get().pluginId)
}
dependencies {
add("ksp", libs.findLibrary("hilt.compiler").get())
add("implementation", libs.findLibrary("hilt.android").get())
add("implementation", libs.findLibrary("hilt.android.testing").get())
add("implementation", libs.findLibrary("hilt.core").get())
add("implementation", libs.findLibrary("hilt.navigation.compose").get())
}
}
}
}
플러그인을 작성한 후에는 build-logic의 빌드 스크립트에 등록해주어야한다.
gradlePlugin{
plugins{
register("androidApplicationConventionPlugin"){
id = "skele.android.application"
implementationClass = "AndroidApplicationConventionPlugin"
}
register("androidLibraryConventionPlugin"){
id = "skele.android.library"
implementationClass = "AndroidLibraryConventionPlugin"
}
register("androidApplicationComposeConventionPlugin"){
id = "skele.android.application.compose"
implementationClass = "AndroidApplicationComposeConventionPlugin"
}
register("androidLibraryComposeConventionPlugin"){
id = "skele.android.library.compose"
implementationClass = "AndroidLibraryComposeConventionPlugin"
}
register("androidHiltConventionPlugin"){
id = "skele.android.hilt"
implementationClass = "HiltConventionPlugin"
}
}
}
또한 이렇게 등록된 플러그인을 다른 모듈에서 간편하게 사용하기 위해서 버전 카탈로그에 등록한다.
# custom plugins
module-android-application = { id = "skele.android.application", version = "unspecified" }
module-android-library = { id = "skele.android.library", version = "unspecified" }
module-android-application-compose = { id = "skele.android.application.compose", version = "unspecified" }
module-android-library-compose = { id = "skele.android.library.compose", version = "unspecified" }
module-hilt = { id = "skele.android.hilt", version = "unspecified" }
커스텀 플러그인을 모듈에 적용시키기에 앞서 플러그인에 사용되는 ksp, hilt와 같은 공용 플러그인을 프로젝트에 사용하기 위해 프로젝트 레벨 빌드 스크립트에 선언을 해야한다.
plugins {
alias(libs.plugins.android.application) apply false
alias(libs.plugins.android.library) apply false
alias(libs.plugins.kotlin.android) apply false
alias(libs.plugins.kotlin.compose) apply false
alias(libs.plugins.kotlin.serialization) apply false
alias(libs.plugins.ksp) apply false
alias(libs.plugins.hilt) apply false
}
이렇게 등록된 커스텀 플러그인은 다른 라이브러리와 같이 빌드 스크립트에 설정하여 사용할 수 있다.
plugins {
alias(libs.plugins.module.android.application.compose)
}
원래는 dependency 아래에 있던 수많은 implementation을 플러그인으로 만듦으로써 단 한 줄로 대체할 수 있게 되었다.
같은 설정의 다른 앱 모듈을 만들더라도 수많은 의존성 코드를 작성할 필요없이 build-logic에 정의된 플러그인을 재사용하여 불필요한 반복코드를 줄일 수 있게 되는 것이다.