언제부터인가 멀티모듈을 사용하면서 중복으로 사용되고 있는 코드를 줄이기 위해서 build-logic 모듈을 만들어서 사용하고 있었습니다.
(nowinandroid 프로젝트를 참고해서 만들었습니다)
매번 생성할때마다 감사하게도 많은분들이 블로그에 생성 방법을 남겨주셔서 저도 잊지 않기 위해 그리고 글을 읽어주시는 분들을 위해 build-logic 모듈 만들기 글을 남기도록 하겠습니다.
1. build-logic 모듈 생성하기
File -> New -> New Module 메뉴를 클릭합니다.
그 다음 Templates 에서 `Java or Kotlin Library` 를 선택해주고
Library name 에 `build-logic:convention` 을 기입해줍니다
Package Name 에는 기존 패키지 뒤에 convention 이 붙는 그대로 사용했습니다.
Class Name 의 MyClass는 추후 삭제(혹은 변경)할 파일입니다.
Finish 버튼을 클릭하여 모듈 생성이 완료되었다면 Android View 에서 Project View 로 변경해주세요
그리고 프로젝트의 settings.gradle.kts 파일을 열어주세요
include(":build-logic:convention") 부분을 삭제해주시고 아래 스크린샷 처럼
pluginManagement 구문아래에 includeBuild("build-logic") 을 추가해줍니다.
이유로는 gradle 에서는 일반 Kotlin 모듈과 Build Module을 구별할 수 없기 때문입니다.
2. build-logic 모듈 설정
그 다음 build-logic root 레벨에 settings.gradle.kts 파일을 생성한 뒤 아래 내용을 입력해줍니다.
(아래 내용은 version catalog 를 사용하고 있을때의 경우입니다. 최근 프로젝트 생성을 하니 버전 카탈로그가 기본이 되었더군요!)
// settings.gradle.kts (build-logic)
pluginManagement {
repositories {
gradlePluginPortal()
google()
}
}
dependencyResolutionManagement {
repositories {
google {
content {
includeGroupByRegex("com\\.android.*")
includeGroupByRegex("com\\.google.*")
includeGroupByRegex("androidx.*")
}
}
mavenCentral()
}
versionCatalogs {
create("libs") {
from(files("../gradle/libs.versions.toml"))
}
}
}
rootProject.name = "build-logic"
include(":convention")
그리고 gradle.properties 파일도 추가해줍니다.
// gradle.properties (build-logic)
org.gradle.parallel=true
org.gradle.caching=true
org.gradle.configureondemand=true
org.gradle.configuration-cache=true
org.gradle.configuration-cache.parallel=true
다음으로는 build-logic:convention 내 build.gradle.kts 코드를 수정합니다.
[기존 코드(Default)]
[수정 후]
저는 자바 17 버전을 바라보게 했습니다. 기존 11 버전을 보게해도 상관없습니다.
plugins {
`kotlin-dsl`
}
group = "alex.dh.kim.buildlogic"
java {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
kotlin {
compilerOptions {
jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17
}
}
dependencies {
// 버전카탈로그에 정의한 라이브러리를 기입하세요
compileOnly(libs.android.gradlePlugin)
compileOnly(libs.android.tools.common)
compileOnly(libs.compose.gradlePlugin)
compileOnly(libs.kotlin.gradlePlugin)
compileOnly(libs.ksp.gradlePlugin)
}
implementation이 아닌 compileOnly인 이유는 build-logic 모듈에서 사용하는 플러그인은 컴파일중에만 관련이 있기 때문입니다.
NowInAndroid 프로젝트를 참고하면 아래와 같이 정의 되어있습니다. 필요한 라이브러리만 정의하여 사용하시면 됩니다.
# Dependencies of the included build-logic
android-gradlePlugin = { group = "com.android.tools.build", name = "gradle", version.ref = "androidGradlePlugin" }
android-tools-common = { group = "com.android.tools", name = "common", version.ref = "androidTools" }
compose-gradlePlugin = { module = "org.jetbrains.kotlin:compose-compiler-gradle-plugin", version.ref = "kotlin" }
firebase-crashlytics-gradlePlugin = { group = "com.google.firebase", name = "firebase-crashlytics-gradle", version.ref = "firebaseCrashlyticsPlugin" }
firebase-performance-gradlePlugin = { group = "com.google.firebase", name = "perf-plugin", version.ref = "firebasePerfPlugin" }
kotlin-gradlePlugin = { group = "org.jetbrains.kotlin", name = "kotlin-gradle-plugin", version.ref = "kotlin" }
ksp-gradlePlugin = { group = "com.google.devtools.ksp", name = "com.google.devtools.ksp.gradle.plugin", version.ref = "ksp" }
room-gradlePlugin = { group = "androidx.room", name = "room-gradle-plugin", version.ref = "room" }
여기까지 하셨다면 우선 build-logic 모듈은 생성 완료입니다.
하지만 생성만 했다고 사용이 되는것은 아니죠?
Custom Convention Plugin을 만들고 앱 모듈에 적용해보도록 하겠습니다.
3. Custom Convention Plugin 구현
우선 convention 패키지 아래 3개 파일을 추가합니다.
(1) AndroidCompose.kt
package alex.dh.kim.convention
import com.android.build.api.dsl.CommonExtension
import org.gradle.api.Project
import org.gradle.api.provider.Provider
import org.gradle.kotlin.dsl.configure
import org.gradle.kotlin.dsl.dependencies
import org.jetbrains.kotlin.compose.compiler.gradle.ComposeCompilerGradlePluginExtension
/**
* Configure Compose-specific options
*/
internal fun Project.configureAndroidCompose(
commonExtension: CommonExtension<*, *, *, *, *, *>,
) {
commonExtension.apply {
buildFeatures {
compose = true
}
dependencies {
val bom = libs.findLibrary("androidx-compose-bom").get()
"implementation"(platform(bom))
"androidTestImplementation"(platform(bom))
"implementation"(libs.findLibrary("androidx-ui-tooling-preview").get())
"debugImplementation"(libs.findLibrary("androidx-ui-tooling").get())
}
testOptions {
unitTests {
// For Robolectric
isIncludeAndroidResources = true
}
}
}
extensions.configure<ComposeCompilerGradlePluginExtension> {
fun Provider<String>.onlyIfTrue() = flatMap { provider { it.takeIf(String::toBoolean) } }
fun Provider<*>.relativeToRootProject(dir: String) = map {
isolated.rootProject.projectDirectory
.dir("build")
.dir(projectDir.toRelativeString(rootDir))
}.map { it.dir(dir) }
project.providers.gradleProperty("enableComposeCompilerMetrics").onlyIfTrue()
.relativeToRootProject("compose-metrics")
.let(metricsDestination::set)
project.providers.gradleProperty("enableComposeCompilerReports").onlyIfTrue()
.relativeToRootProject("compose-reports")
.let(reportsDestination::set)
stabilityConfigurationFiles
.add(isolated.rootProject.projectDirectory.file("compose_compiler_config.conf"))
}
}
(2) KotlinAndroid.kt
package alex.dh.kim.convention
import com.android.build.api.dsl.CommonExtension
import org.gradle.api.JavaVersion
import org.gradle.api.Project
import org.gradle.api.plugins.JavaPluginExtension
import org.gradle.kotlin.dsl.assign
import org.gradle.kotlin.dsl.configure
import org.gradle.kotlin.dsl.dependencies
import org.gradle.kotlin.dsl.provideDelegate
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
import org.jetbrains.kotlin.gradle.dsl.KotlinAndroidProjectExtension
import org.jetbrains.kotlin.gradle.dsl.KotlinBaseExtension
import org.jetbrains.kotlin.gradle.dsl.KotlinJvmProjectExtension
/**
* Configure base Kotlin with Android options
*/
internal fun Project.configureKotlinAndroid(
commonExtension: CommonExtension<*, *, *, *, *, *>,
) {
commonExtension.apply {
compileSdk = 35
defaultConfig {
minSdk = 26
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
isCoreLibraryDesugaringEnabled = true
}
}
configureKotlin<KotlinAndroidProjectExtension>()
dependencies {
"coreLibraryDesugaring"(libs.findLibrary("android.desugarJdkLibs").get())
}
}
/**
* Configure base Kotlin options for JVM (non-Android)
*/
internal fun Project.configureKotlinJvm() {
extensions.configure<JavaPluginExtension> {
// Up to Java 11 APIs are available through desugaring
// https://developer.android.com/studio/write/java11-minimal-support-table
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
configureKotlin<KotlinJvmProjectExtension>()
}
/**
* Configure base Kotlin options
*/
private inline fun <reified T : KotlinBaseExtension> Project.configureKotlin() = configure<T> {
// Treat all Kotlin warnings as errors (disabled by default)
// Override by setting warningsAsErrors=true in your ~/.gradle/gradle.properties
val warningsAsErrors: String? by project
when (this) {
is KotlinAndroidProjectExtension -> compilerOptions
is KotlinJvmProjectExtension -> compilerOptions
else -> TODO("Unsupported project extension $this ${T::class}")
}.apply {
jvmTarget = JvmTarget.JVM_17
allWarningsAsErrors = warningsAsErrors.toBoolean()
freeCompilerArgs.add(
// Enable experimental coroutines APIs, including Flow
"-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi",
)
freeCompilerArgs.add(
/**
* Remove this args after Phase 3.
* https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-consistent-copy-visibility/#deprecation-timeline
*
* Deprecation timeline
* Phase 3. (Supposedly Kotlin 2.2 or Kotlin 2.3).
* The default changes.
* Unless ExposedCopyVisibility is used, the generated 'copy' method has the same visibility as the primary constructor.
* The binary signature changes. The error on the declaration is no longer reported.
* '-Xconsistent-data-class-copy-visibility' compiler flag and ConsistentCopyVisibility annotation are now unnecessary.
*/
"-Xconsistent-data-class-copy-visibility"
)
}
}
(3) ProjectExtensions.kt
package alex.dh.kim.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
get(): VersionCatalog = extensions.getByType<VersionCatalogsExtension>().named("libs")
그리고 패키지 밖 영역에 2개 파일을 생성합니다.
AndroidApplicationConventionPlugin.kt
- 안드로이드 어플리케이션에 대한 Convention Plugin 입니다. application 에서 사용되는 공통 플러그인을 정의해주시면 됩니다.
import alex.dh.kim.convention.configureKotlinAndroid
import com.android.build.api.dsl.ApplicationExtension
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.kotlin.dsl.apply
import org.gradle.kotlin.dsl.configure
class AndroidApplicationConventionPlugin : Plugin<Project> {
override fun apply(target: Project) {
with(target) {
apply(plugin = "com.android.application")
apply(plugin = "org.jetbrains.kotlin.android")
extensions.configure<ApplicationExtension> {
configureKotlinAndroid(this)
defaultConfig.targetSdk = 35
@Suppress("UnstableApiUsage")
testOptions.animationsDisabled = true
}
}
}
}
AndroidApplicationComposeConventionPlugin.kt
- Android Application Compose 에서 사용되는 Convention Plugin 입니다.
import alex.dh.kim.convention.configureAndroidCompose
import com.android.build.api.dsl.ApplicationExtension
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.kotlin.dsl.apply
import org.gradle.kotlin.dsl.getByType
class AndroidApplicationComposeConventionPlugin : Plugin<Project> {
override fun apply(target: Project) {
with(target) {
apply(plugin = "com.android.application")
apply(plugin = "org.jetbrains.kotlin.plugin.compose")
val extension = extensions.getByType<ApplicationExtension>()
configureAndroidCompose(extension)
}
}
}
해당 컨벤션에 대한 정의를 convention 영역의 build.gradle.kts 파일에서 정의합니다.
gradlePlugin {
plugins {
register("androidApplication") {
id = libs.plugins.alex.android.application.asProvider().get().pluginId
implementationClass = "AndroidApplicationConventionPlugin"
}
register("androidApplicationCompose") {
id = libs.plugins.alex.android.application.compose.get().pluginId
implementationClass = "AndroidApplicationComposeConventionPlugin"
}
}
}
id 에 들어가는 내용은 version catalog 에 정의한 부분입니다.
# Plugins defined by this project
alex-android-application = { id = "alex.android.application" }
alex-android-application-compose = { id = "alex.android.application.compose" }
해당 부분까지 추가하신 뒤, gradle sync 해주시고 app 모듈의 build.gradle.kts 로 이동하겠습니다.
Plugins 부분을 정의한 id 로 적용합니다.
plugins {
alias(libs.plugins.alex.android.application)
alias(libs.plugins.alex.android.application.compose)
}
그 외 중복으로 사용되는 compileSdk, minSdk, targetSdk, compileOptions, kotlinOptions 를 제거하면 됩니다.
만약 No value present 에러가 발생했다면 Custom Convention Plugin 에서 정의한 라이브러리가 version catalog에 없기 때문일 수 있습니다. 저의 경우에는 desugarJdkLibs 를 정의해주지 않아서 에러가 발생했습니다.
[versions]
androidDesugarJdkLibs = "2.1.5"
[libraries]
android-desugarJdkLibs = { group = "com.android.tools", name = "desugar_jdk_libs", version.ref = "androidDesugarJdkLibs" }
이렇게 하고 gradle sync 를 하게 되면 정상적으로 싱크가 되는것을 확인할 수 있으며 빌드시에도 정상 빌드됨을 확인할 수 있습니다.
NowInAndroid 프로젝트에는 더욱 많은 커스텀 컨벤션이 있으니 해당 내용을 참고하셔서 필요한 컨벤션 플러그인을 정의하시면 됩니다.
긴글 읽어주셔서 고맙습니다.
'개발자 > Android' 카테고리의 다른 글
[Android] Kotlin DSL로 Gradle build script 변경하기 (0) | 2023.01.08 |
---|---|
[Android/Kotlin] Kotlin 의 val 에 대해서 (0) | 2017.09.19 |
[Android] 액티비티내 캡처 방지하기 (0) | 2017.09.19 |
[Android] RGB Alpha 퍼센트에 따른 Hex값 (0) | 2017.02.28 |
[Android/Kotlin] 코틀린(Kotlin) 사용해보기 #1 (0) | 2016.12.23 |
댓글