Initial commit
Some checks failed
CI / build (push) Has been cancelled

This commit is contained in:
2026-06-11 11:03:01 +02:00
commit d1ff0e30ba
138 changed files with 5658 additions and 0 deletions

View File

@@ -0,0 +1,71 @@
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
plugins {
`kotlin-dsl`
}
group = "com.example.architecture.buildlogic"
java {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
tasks.withType<KotlinCompile>().configureEach {
compilerOptions {
jvmTarget = JvmTarget.JVM_17
}
}
dependencies {
// The convention plugins apply these by id, so they only need them at compile time.
compileOnly(libs.android.gradlePlugin)
compileOnly(libs.kotlin.gradlePlugin)
compileOnly(libs.compose.compiler.gradlePlugin)
}
gradlePlugin {
plugins {
register("androidApplication") {
id = "architecture.android.application"
implementationClass = "com.example.architecture.convention.AndroidApplicationConventionPlugin"
}
register("androidLibrary") {
id = "architecture.android.library"
implementationClass = "com.example.architecture.convention.AndroidLibraryConventionPlugin"
}
register("androidFeature") {
id = "architecture.android.feature"
implementationClass = "com.example.architecture.convention.AndroidFeatureConventionPlugin"
}
register("androidFeatureViews") {
id = "architecture.android.feature.views"
implementationClass = "com.example.architecture.convention.AndroidFeatureViewsConventionPlugin"
}
register("domainModule") {
id = "architecture.domain.module"
implementationClass = "com.example.architecture.convention.DomainModuleConventionPlugin"
}
register("androidUnitTest") {
id = "architecture.android.unit.test"
implementationClass = "com.example.architecture.convention.AndroidUnitTestConventionPlugin"
}
register("compose") {
id = "architecture.compose"
implementationClass = "com.example.architecture.convention.ComposeConventionPlugin"
}
register("koin") {
id = "architecture.koin"
implementationClass = "com.example.architecture.convention.KoinConventionPlugin"
}
register("ktor") {
id = "architecture.ktor"
implementationClass = "com.example.architecture.convention.KtorConventionPlugin"
}
register("kotlinxSerialization") {
id = "architecture.kotlinx.serialization"
implementationClass = "com.example.architecture.convention.KotlinxSerializationConventionPlugin"
}
}
}

View File

@@ -0,0 +1,57 @@
package com.example.architecture.convention
import com.android.build.api.dsl.ApplicationExtension
import org.gradle.api.JavaVersion
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.kotlin.dsl.configure
/**
* Configures the single `:app` module: applicationId, SDK levels, Java 17, and the release
* build type. Compose is added separately by [ComposeConventionPlugin].
*/
class AndroidApplicationConventionPlugin : Plugin<Project> {
override fun apply(target: Project) = with(target) {
pluginManager.apply("com.android.application")
extensions.configure<ApplicationExtension> {
namespace = "com.example.architecture"
compileSdk = COMPILE_SDK
defaultConfig {
applicationId = "com.example.architecture"
minSdk = MIN_SDK
targetSdk = TARGET_SDK
versionCode = 1
versionName = "1.0"
}
buildTypes {
getByName("release") {
isMinifyEnabled = false
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro",
)
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
buildFeatures {
buildConfig = false
}
packaging {
resources {
excludes += "/META-INF/{AL2.0,LGPL2.1}"
}
}
}
configureKotlinJvmToolchain()
}
}

View File

@@ -0,0 +1,28 @@
package com.example.architecture.convention
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.kotlin.dsl.dependencies
/**
* A Compose-based feature presentation module: Android library + Compose + Koin, plus the common
* feature stack (lifecycle, type-safe navigation, coroutines, Coil). Used by every
* `:feature:*:presentation-compose` and the MVVM `:feature:about:presentation`.
*/
class AndroidFeatureConventionPlugin : Plugin<Project> {
override fun apply(target: Project) = with(target) {
pluginManager.apply("architecture.android.library")
pluginManager.apply("architecture.compose")
pluginManager.apply("architecture.koin")
dependencies {
add("implementation", libs.findLibrary("androidx-core-ktx").get())
add("implementation", libs.findBundle("lifecycle-compose").get())
add("implementation", libs.findLibrary("androidx-navigation-compose").get())
add("implementation", libs.findLibrary("kotlinx-coroutines-android").get())
add("implementation", libs.findLibrary("koin-androidx-compose").get())
add("implementation", libs.findLibrary("coil-compose").get())
add("implementation", libs.findLibrary("coil-network-okhttp").get())
}
}
}

View File

@@ -0,0 +1,33 @@
package com.example.architecture.convention
import com.android.build.api.dsl.LibraryExtension
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.kotlin.dsl.configure
import org.gradle.kotlin.dsl.dependencies
/**
* A classic Views feature renderer: Android library + Koin, ViewBinding ON, Compose OFF.
* Brings Fragment / RecyclerView / Material / AppCompat and Coil's ImageView loader so the
* Views renderer can drive the same ViewModel as the Compose one.
*/
class AndroidFeatureViewsConventionPlugin : Plugin<Project> {
override fun apply(target: Project) = with(target) {
pluginManager.apply("architecture.android.library")
pluginManager.apply("architecture.koin")
extensions.configure<LibraryExtension> {
buildFeatures.viewBinding = true
}
dependencies {
add("implementation", libs.findLibrary("androidx-core-ktx").get())
add("implementation", libs.findBundle("views").get())
add("implementation", libs.findLibrary("androidx-lifecycle-runtime-ktx").get())
add("implementation", libs.findLibrary("androidx-lifecycle-viewmodel-ktx").get())
add("implementation", libs.findLibrary("kotlinx-coroutines-android").get())
add("implementation", libs.findLibrary("coil-core").get())
add("implementation", libs.findLibrary("coil-network-okhttp").get())
}
}
}

View File

@@ -0,0 +1,35 @@
package com.example.architecture.convention
import com.android.build.api.dsl.LibraryExtension
import org.gradle.api.JavaVersion
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.kotlin.dsl.configure
/**
* Base configuration shared by every Android library module. Each module still declares its own
* `namespace` in its build file.
*/
class AndroidLibraryConventionPlugin : Plugin<Project> {
override fun apply(target: Project) = with(target) {
pluginManager.apply("com.android.library")
extensions.configure<LibraryExtension> {
compileSdk = COMPILE_SDK
defaultConfig {
minSdk = MIN_SDK
// Used by instrumented (androidTest) tests, e.g. the Compose UI test in
// :feature:characters:presentation-compose. Harmless for modules without androidTest.
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
}
configureKotlinJvmToolchain()
}
}

View File

@@ -0,0 +1,32 @@
package com.example.architecture.convention
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.api.tasks.testing.Test
import org.gradle.kotlin.dsl.dependencies
import org.gradle.kotlin.dsl.withType
/**
* Runs an Android library module's local unit tests (`src/test`) on the **JUnit 5 platform** with the
* shared `unit-test` toolset (JUnit Jupiter, kotlinx-coroutines-test, Turbine, AssertK).
*
* Deliberately does NOT use the `de.mannodermaus.android-junit5` Gradle plugin: its 1.11.x line
* targets AGP 8.x and we build on AGP 9.0. It isn't needed for *local* unit tests anyway -
* `com.android.build.gradle.tasks.factory.AndroidUnitTest` extends Gradle's [Test] task, so calling
* `useJUnitPlatform()` on it is enough (this mirrors `DomainModuleConventionPlugin`, which does the
* same for pure-JVM modules).
*/
class AndroidUnitTestConventionPlugin : Plugin<Project> {
override fun apply(target: Project) = with(target) {
dependencies {
add("testImplementation", libs.findBundle("unit-test").get())
add("testRuntimeOnly", libs.findLibrary("junit-jupiter-engine").get())
// Gradle 9 dropped the bundled launcher; JUnit 5 won't start without it.
add("testRuntimeOnly", libs.findLibrary("junit-platform-launcher").get())
}
tasks.withType<Test>().configureEach {
useJUnitPlatform()
}
}
}

View File

@@ -0,0 +1,39 @@
package com.example.architecture.convention
import com.android.build.api.dsl.ApplicationExtension
import com.android.build.api.dsl.LibraryExtension
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.kotlin.dsl.configure
import org.gradle.kotlin.dsl.dependencies
/**
* Enables Jetpack Compose on an Android application or library module: applies the Compose
* compiler plugin, turns on the `compose` build feature, and wires the BOM-aligned Compose deps.
*
* Order-independent: `withPlugin` enables the build feature whenever the Android plugin is applied,
* regardless of whether this plugin runs before or after it.
*/
class ComposeConventionPlugin : Plugin<Project> {
override fun apply(target: Project) = with(target) {
pluginManager.apply("org.jetbrains.kotlin.plugin.compose")
pluginManager.withPlugin("com.android.library") {
extensions.configure<LibraryExtension> { buildFeatures.compose = true }
}
pluginManager.withPlugin("com.android.application") {
extensions.configure<ApplicationExtension> { buildFeatures.compose = true }
}
dependencies {
// `implementation` (not api): every Compose consumer applies this convention itself, so
// Compose must NOT leak transitively - that keeps the UI-agnostic presentation module
// (which depends on core:presentation) free of Compose.
val bom = platform(libs.findLibrary("androidx-compose-bom").get())
add("implementation", bom)
add("androidTestImplementation", bom)
add("implementation", libs.findBundle("compose").get())
add("debugImplementation", libs.findLibrary("androidx-compose-ui-tooling").get())
}
}
}

View File

@@ -0,0 +1,36 @@
package com.example.architecture.convention
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.api.tasks.testing.Test
import org.gradle.kotlin.dsl.dependencies
import org.gradle.kotlin.dsl.withType
/**
* Pure-Kotlin (JVM) module for the domain layer: no Android dependencies. Adds Coroutines (for
* `Flow`-returning repository interfaces) and runs unit tests on the JUnit 5 platform.
*/
class DomainModuleConventionPlugin : Plugin<Project> {
override fun apply(target: Project) = with(target) {
pluginManager.apply("org.jetbrains.kotlin.jvm")
configureKotlinJvmToolchain()
dependencies {
add("implementation", libs.findLibrary("kotlinx-coroutines-core").get())
add("testImplementation", libs.findLibrary("junit-jupiter-api").get())
add("testImplementation", libs.findLibrary("assertk").get())
// Domain doesn't consume the `unit-test` bundle, so MockK and the coroutines
// test artifact are added explicitly here.
add("testImplementation", libs.findLibrary("mockk").get())
add("testImplementation", libs.findLibrary("kotlinx-coroutines-test").get())
add("testRuntimeOnly", libs.findLibrary("junit-jupiter-engine").get())
// Gradle 9 dropped the bundled launcher; JUnit 5 won't start without it.
add("testRuntimeOnly", libs.findLibrary("junit-platform-launcher").get())
}
tasks.withType<Test>().configureEach {
useJUnitPlatform()
}
}
}

View File

@@ -0,0 +1,20 @@
package com.example.architecture.convention
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.kotlin.dsl.dependencies
/**
* Adds the Koin BOM + core/android dependencies. Compose-specific Koin (`koin-androidx-compose`)
* is added only by [AndroidFeatureConventionPlugin] so UI-agnostic modules stay Compose-free.
*/
class KoinConventionPlugin : Plugin<Project> {
override fun apply(target: Project) = with(target) {
dependencies {
val bom = platform(libs.findLibrary("koin-bom").get())
add("implementation", bom)
add("implementation", libs.findBundle("koin").get())
add("testImplementation", libs.findLibrary("koin-test").get())
}
}
}

View File

@@ -0,0 +1,19 @@
package com.example.architecture.convention
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.kotlin.dsl.dependencies
/**
* Applies the KotlinX Serialization compiler plugin + JSON runtime for modules that hold
* `@Serializable` DTOs or navigation routes but do not need the full Ktor stack.
*/
class KotlinxSerializationConventionPlugin : Plugin<Project> {
override fun apply(target: Project) = with(target) {
pluginManager.apply("org.jetbrains.kotlin.plugin.serialization")
dependencies {
add("implementation", libs.findLibrary("kotlinx-serialization-json").get())
}
}
}

View File

@@ -0,0 +1,22 @@
package com.example.architecture.convention
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.kotlin.dsl.dependencies
/**
* Wires the Ktor client bundle (OkHttp engine, content negotiation, JSON, logging) and the
* KotlinX Serialization runtime. Applies the serialization compiler plugin so `@Serializable`
* DTOs compile.
*/
class KtorConventionPlugin : Plugin<Project> {
override fun apply(target: Project) = with(target) {
pluginManager.apply("org.jetbrains.kotlin.plugin.serialization")
dependencies {
add("implementation", libs.findBundle("ktor").get())
add("implementation", libs.findLibrary("kotlinx-serialization-json").get())
add("testImplementation", libs.findLibrary("ktor-client-mock").get())
}
}
}

View File

@@ -0,0 +1,27 @@
package com.example.architecture.convention
import org.gradle.api.Project
import org.gradle.api.artifacts.VersionCatalog
import org.gradle.api.artifacts.VersionCatalogsExtension
import org.gradle.kotlin.dsl.configure
import org.gradle.kotlin.dsl.getByType
import org.jetbrains.kotlin.gradle.dsl.KotlinProjectExtension
internal const val COMPILE_SDK = 36
internal const val MIN_SDK = 24
internal const val TARGET_SDK = 36
internal const val JVM_TARGET = 17
/** Type-safe accessor for the shared `libs` version catalog from inside a convention plugin. */
internal val Project.libs: VersionCatalog
get() = extensions.getByType<VersionCatalogsExtension>().named("libs")
/**
* Pins the Kotlin JVM toolchain. Works for both Android modules and pure-Kotlin (`jvm`) modules
* because [KotlinProjectExtension] is the common supertype of both kotlin extensions.
*/
internal fun Project.configureKotlinJvmToolchain() {
extensions.configure<KotlinProjectExtension> {
jvmToolchain(JVM_TARGET)
}
}

View File

@@ -0,0 +1,26 @@
@file:Suppress("UnstableApiUsage")
pluginManagement {
repositories {
google()
mavenCentral()
gradlePluginPortal()
}
}
dependencyResolutionManagement {
repositories {
google()
mavenCentral()
gradlePluginPortal()
}
versionCatalogs {
// Reuse the single source of truth for versions.
create("libs") {
from(files("../gradle/libs.versions.toml"))
}
}
}
rootProject.name = "build-logic"
include(":convention")