chore: scaffold multi-module project, version catalog, and build-logic

Foundation milestone (REDI-78, REDI-79):

- Multi-module skeleton: :app, :core:{domain,data,presentation,design-system},
  :feature:characters:{domain,data,presentation,presentation-compose,presentation-views},
  :feature:about:presentation, plus the :build-logic composite build.
- gradle/libs.versions.toml as the single source of truth ([versions]/[libraries]/
  [bundles]/[plugins]); no inline versions in any build file.
- Convention plugins: architecture.android.{application,library,feature,feature.views},
  domain.module, compose, koin, ktor, kotlinx.serialization.
- Pure-Kotlin domain modules; presentation-compose uses android.feature;
  presentation-views uses android.feature.views (ViewBinding on, Compose off);
  the UI-agnostic :presentation has neither Compose nor Views deps.
- Toolchain: AGP 9.0.1, Kotlin 2.3.20, Gradle 9.1.0, compileSdk 36, minSdk 24, Java 17.
- Minimal MainActivity placeholder; CI (assembleDebug) via GitHub Actions.

Verified: ./gradlew projects lists the full tree and ./gradlew assemble is green.
This commit is contained in:
2026-06-10 10:52:03 +02:00
commit 10fa6dc9eb
56 changed files with 1468 additions and 0 deletions

View File

@@ -0,0 +1,67 @@
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("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,32 @@
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
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
}
configureKotlinJvmToolchain()
}
}

View File

@@ -0,0 +1,36 @@
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 {
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,30 @@
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())
add("testRuntimeOnly", libs.findLibrary("junit-jupiter-engine").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)
}
}