commit 10fa6dc9eb3ccd956e5694b27f0b16818102c1d5 Author: Adrian Kuta Date: Wed Jun 10 10:52:03 2026 +0200 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. diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..a8d918f --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,36 @@ +name: CI + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up JDK 17 + uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: '17' + + - name: Validate Gradle wrapper + uses: gradle/actions/wrapper-validation@v4 + + - name: Set up Android SDK + uses: android-actions/setup-android@v3 + + - name: Set up Gradle + uses: gradle/actions/setup-gradle@v4 + + - name: Assemble (debug) + run: ./gradlew assembleDebug --no-daemon --stacktrace diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..fbdb9c9 --- /dev/null +++ b/.gitignore @@ -0,0 +1,27 @@ +*.iml +.DS_Store + +# Gradle +**/.gradle/ +**/build/ +/captures + +# Kotlin +.kotlin/ + +# Native +.externalNativeBuild +.cxx + +# Local config / secrets +local.properties + +# IDE +/.idea/caches +/.idea/libraries +/.idea/modules.xml +/.idea/workspace.xml +/.idea/navEditor.xml +/.idea/assetWizardSettings.xml +/.idea/deploymentTargetSelector.xml +/.idea/deploymentTargetDropDown.xml diff --git a/README.md b/README.md new file mode 100644 index 0000000..8d6c5fb --- /dev/null +++ b/README.md @@ -0,0 +1,52 @@ +# Android Architecture Showcase + +A single runnable **Android-only (Jetpack Compose)** reference app that demonstrates good +architecture conventions — each in its own module/example. Teaching repo: every module is meant to +be minimal but complete and idiomatic. + +> **Status:** built milestone-by-milestone from the +> [Linear backlog](https://linear.app/adrian-kuta/project/android-architecture-showcase-b5ecdeddda6c). +> **Foundation** (scaffold, version catalog, `build-logic` convention plugins) is complete and the +> project assembles green. Full architecture docs land with the *Quality & Docs* milestone. + +## Stack + +Multi-module Gradle + `build-logic` convention plugins · Koin (constructor DSL) · Ktor · +KotlinX Serialization · Coil · Kermit · type-safe Compose Navigation. Data comes from the no-key +[Rick & Morty API](https://rickandmortyapi.com/). + +What it will showcase: **MVI** as the primary presentation pattern (flagship *characters* feature), +an **MVVM** contrast screen, and the same MVI `ViewModel` driven by **two renderers** — Jetpack +Compose and classic **XML + ViewBinding + RecyclerView** — proving the presentation logic is +UI-toolkit-agnostic. + +## Module structure + +``` +:app → wires everything; single Activity, Compose host +:build-logic → Gradle convention plugins (the only place versions/config live) +:core:domain → Result/error types, shared domain models (pure Kotlin) +:core:data → Ktor HttpClient factory, safe-call helpers +:core:presentation → UiText, ObserveAsEvents, DataError → UiText +:core:design-system → AppTheme + reusable composables +:feature:characters:domain → models + repository interface (pure Kotlin) +:feature:characters:data → DTOs, mappers, data source, repository impl +:feature:characters:presentation → MVI ViewModel/State/Action/Event (UI-agnostic: no Compose, no Views) +:feature:characters:presentation-compose → Compose renderer +:feature:characters:presentation-views → Views/XML renderer (same ViewModel) +:feature:about:presentation → MVVM contrast screen +``` + +**Dependency rules:** `presentation → domain ← data`; `domain` depends only on `:core:domain`; +features never depend on other features; `:app` wires the graph. + +## Build & run + +```bash +./gradlew assembleDebug # build the APK +./gradlew projects # print the module tree +./gradlew check # tests + lint (added in the Quality & Docs milestone) +``` + +Requires JDK 17+ (the Gradle build pins a Java 17 toolchain) and the Android SDK +(`compileSdk 36`, `minSdk 24`). diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/app/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/app/build.gradle.kts b/app/build.gradle.kts new file mode 100644 index 0000000..5469aab --- /dev/null +++ b/app/build.gradle.kts @@ -0,0 +1,14 @@ +plugins { + alias(libs.plugins.architecture.android.application) + alias(libs.plugins.architecture.compose) +} + +dependencies { + implementation(libs.androidx.core.ktx) + implementation(libs.androidx.activity.compose) + implementation(libs.androidx.lifecycle.runtime.ktx) + implementation(libs.bundles.lifecycle.compose) + + androidTestImplementation(libs.androidx.compose.ui.test.junit4) + debugImplementation(libs.androidx.compose.ui.test.manifest) +} diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 0000000..403053c --- /dev/null +++ b/app/proguard-rules.pro @@ -0,0 +1,2 @@ +# Add project-specific ProGuard rules here. +# Minification is disabled for the release build type in this teaching project. diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..778f466 --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + diff --git a/app/src/main/java/com/example/architecture/MainActivity.kt b/app/src/main/java/com/example/architecture/MainActivity.kt new file mode 100644 index 0000000..e3e00e9 --- /dev/null +++ b/app/src/main/java/com/example/architecture/MainActivity.kt @@ -0,0 +1,36 @@ +package com.example.architecture + +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier + +class MainActivity : ComponentActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContent { + // Placeholder content. The real navigation host + AppTheme are wired in later + // milestones (design-system, characters graph, Koin bootstrap). + MaterialTheme { + Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding -> + Box( + modifier = Modifier + .fillMaxSize() + .padding(innerPadding), + contentAlignment = Alignment.Center, + ) { + Text(text = "Android Architecture Showcase") + } + } + } + } + } +} diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000..07d5da9 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_launcher_foreground.xml b/app/src/main/res/drawable/ic_launcher_foreground.xml new file mode 100644 index 0000000..2b068d1 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_foreground.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 0000000..6f3b755 --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 0000000..6f3b755 --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/app/src/main/res/mipmap-hdpi/ic_launcher.webp new file mode 100644 index 0000000..c209e78 Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp new file mode 100644 index 0000000..b2dfe3d Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/app/src/main/res/mipmap-mdpi/ic_launcher.webp new file mode 100644 index 0000000..4f0f1d6 Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp new file mode 100644 index 0000000..62b611d Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp new file mode 100644 index 0000000..948a307 Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp new file mode 100644 index 0000000..1b9a695 Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp new file mode 100644 index 0000000..28d4b77 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp new file mode 100644 index 0000000..9287f50 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp new file mode 100644 index 0000000..aa7d642 Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp new file mode 100644 index 0000000..9126ae3 Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml new file mode 100644 index 0000000..6917b29 --- /dev/null +++ b/app/src/main/res/values/strings.xml @@ -0,0 +1,3 @@ + + Android Architecture Showcase + \ No newline at end of file diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml new file mode 100644 index 0000000..0376602 --- /dev/null +++ b/app/src/main/res/values/themes.xml @@ -0,0 +1,9 @@ + + + +