chore(deps): update all libraries and Gradle to latest stable versions

Bump the version catalog, Gradle wrapper, and convention plugins to the
latest stable releases. Verified with `./gradlew assembleDebug test`
(BUILD SUCCESSFUL, 21 unit tests pass).

Toolchain:
- Gradle 9.1.0 -> 9.5.1
- AGP 9.0.1 -> 9.2.1
- Kotlin 2.3.20 -> 2.4.0

Libraries:
- androidx-core 1.18.0 -> 1.19.0, appcompat 1.7.0 -> 1.7.1,
  fragment 1.8.5 -> 1.8.9, navigation 2.9.0 -> 2.9.8
- compose-bom 2026.03.01 -> 2026.05.01, material 1.12.0 -> 1.14.0
- coroutines 1.10.2 -> 1.11.0, serialization 1.8.1 -> 1.11.0,
  collections-immutable 0.3.8 -> 0.5.0
- koin 4.1.0 -> 4.2.1, ktor 3.1.3 -> 3.5.0, coil 3.1.0 -> 3.5.0
- JUnit 5.11.4 -> 6.1.0, turbine 1.2.0 -> 1.2.1, mockk 1.14.3 -> 1.14.11

Required side-effect:
- compileSdk 36 -> 37 (mandated by androidx.core 1.19.0); targetSdk
  left at 36.

Also refresh stale JUnit 5 / AGP 9.0 / compileSdk 36 references in the
README and convention-plugin docs.
This commit is contained in:
2026-06-12 14:53:26 +02:00
parent 9ae6e5935a
commit 04e1dc03e5
7 changed files with 35 additions and 35 deletions

View File

@@ -36,17 +36,17 @@ a small MVVM *About* screen for contrast, and a dedicated **error-handling demo*
| Concern | Choice | | Concern | Choice |
|---|---| |---|---|
| Build | Multi-module Gradle + `:build-logic` **convention plugins**; a single **version catalog** (`gradle/libs.versions.toml`) is the only place versions live | | Build | Multi-module Gradle + `:build-logic` **convention plugins**; a single **version catalog** (`gradle/libs.versions.toml`) is the only place versions live |
| Toolchain | AGP 9.0.1, Kotlin 2.3.20, Gradle 9.1, `compileSdk`/`targetSdk` 36, `minSdk` 24, Java 17 | | Toolchain | AGP 9.2.1, Kotlin 2.4.0, Gradle 9.5.1, `compileSdk` 37 / `targetSdk` 36, `minSdk` 24, Java 17 |
| UI | Jetpack Compose (Material 3) + one classic **Views/XML** renderer | | UI | Jetpack Compose (Material 3) + one classic **Views/XML** renderer |
| DI | Koin 4.1 (constructor DSL) | | DI | Koin 4.2 (constructor DSL) |
| Networking | Ktor (OkHttp engine) + KotlinX Serialization | | Networking | Ktor (OkHttp engine) + KotlinX Serialization |
| Images | Coil 3 | | Images | Coil 3 |
| Navigation | type-safe Compose Navigation (`@Serializable` routes) | | Navigation | type-safe Compose Navigation (`@Serializable` routes) |
| Logging | Timber | | Logging | Timber |
| Async | Coroutines + Flow | | Async | Coroutines + Flow |
| Testing | JUnit 5, MockK, Turbine, AssertK, `kotlinx-coroutines-test`, Ktor `MockEngine`, Compose UI test | | Testing | JUnit 6, MockK, Turbine, AssertK, `kotlinx-coroutines-test`, Ktor `MockEngine`, Compose UI test |
> **AGP 9 gotcha:** AGP 9.0 has **built-in Kotlin**. Applying `com.android.application`/`library` > **AGP 9 gotcha:** AGP 9.2 has **built-in Kotlin**. Applying `com.android.application`/`library`
> auto-applies the Kotlin Android plugin, so the convention plugins must **not** apply > auto-applies the Kotlin Android plugin, so the convention plugins must **not** apply
> `org.jetbrains.kotlin.android` themselves. Source lives in `src/main/kotlin`. > `org.jetbrains.kotlin.android` themselves. Source lives in `src/main/kotlin`.
@@ -276,12 +276,12 @@ both resolve the **same** `CharacterListViewModel` class and supply its `SavedSt
## Testing ## Testing
Tests prove the architecture, not just the code. Stack: **JUnit 5**, **MockK**, **Turbine** (Flow), Tests prove the architecture, not just the code. Stack: **JUnit 6**, **MockK**, **Turbine** (Flow),
**AssertK**, `kotlinx-coroutines-test`, Ktor **`MockEngine`**, and Compose UI test. **AssertK**, `kotlinx-coroutines-test`, Ktor **`MockEngine`**, and Compose UI test.
| What | Where | Kind | | What | Where | Kind |
|---|---|---| |---|---|---|
| `GetCharactersPageUseCase` | `:feature:characters:domain` `src/test` | pure JVM, JUnit 5 | | `GetCharactersPageUseCase` | `:feature:characters:domain` `src/test` | pure JVM, JUnit 6 |
| `CharacterListViewModel`, `CharacterDetailViewModel` | `:feature:characters:presentation` `src/test` | JVM unit, MockK + Turbine + `SavedStateHandle` | | `CharacterListViewModel`, `CharacterDetailViewModel` | `:feature:characters:presentation` `src/test` | JVM unit, MockK + Turbine + `SavedStateHandle` |
| `NetworkCharacterRepository` | `:feature:characters:data` `src/test` | JVM unit, Ktor `MockEngine` | | `NetworkCharacterRepository` | `:feature:characters:data` `src/test` | JVM unit, Ktor `MockEngine` |
| `CharacterListScreen` (robot) | `:feature:characters:presentation-compose` `src/androidTest` | instrumented Compose UI | | `CharacterListScreen` (robot) | `:feature:characters:presentation-compose` `src/androidTest` | instrumented Compose UI |
@@ -301,7 +301,7 @@ Conventions demonstrated:
reads as a scenario; it asserts a rendered item, the empty/error states, and that a tap fires the reads as a scenario; it asserts a rendered item, the empty/error states, and that a tap fires the
right `Action`. right `Action`.
> **JUnit 5 on AGP 9:** the `de.mannodermaus.android-junit5` Gradle plugin targets AGP 8.x, so this > **JUnit 6 on AGP 9:** the `de.mannodermaus.android-junit5` Gradle plugin targets AGP 8.x, so this
> repo doesn't use it. `AndroidUnitTest` extends Gradle's `Test`, so the `architecture.android.unit.test` > repo doesn't use it. `AndroidUnitTest` extends Gradle's `Test`, so the `architecture.android.unit.test`
> convention plugin just calls `useJUnitPlatform()` and adds the `unit-test` bundle - including the > convention plugin just calls `useJUnitPlatform()` and adds the `unit-test` bundle - including the
> `junit-platform-launcher`, which Gradle 9 no longer bundles. > `junit-platform-launcher`, which Gradle 9 no longer bundles.
@@ -323,7 +323,7 @@ possible but intentionally omitted (the VM logic is already covered by the share
# Build # Build
./gradlew assembleDebug # build the debug APK ./gradlew assembleDebug # build the debug APK
./gradlew projects # print the module tree ./gradlew projects # print the module tree
./gradlew test # all JVM unit tests (JUnit 5) ./gradlew test # all JVM unit tests (JUnit 6)
./gradlew :feature:characters:presentation-compose:connectedDebugAndroidTest # Compose UI test (needs a device) ./gradlew :feature:characters:presentation-compose:connectedDebugAndroidTest # Compose UI test (needs a device)
``` ```
@@ -339,7 +339,7 @@ android docs search "<topic>" # search authoritative Android docs
``` ```
Requires JDK 17+ (the Gradle build pins a Java 17 toolchain) and the Android SDK Requires JDK 17+ (the Gradle build pins a Java 17 toolchain) and the Android SDK
(`compileSdk 36`, `minSdk 24`). (`compileSdk 37`, `minSdk 24`).
--- ---

View File

@@ -7,11 +7,11 @@ import org.gradle.kotlin.dsl.dependencies
import org.gradle.kotlin.dsl.withType import org.gradle.kotlin.dsl.withType
/** /**
* Runs an Android library module's local unit tests (`src/test`) on the **JUnit 5 platform** with the * Runs an Android library module's local unit tests (`src/test`) on the **JUnit Platform** with the
* shared `unit-test` toolset (JUnit Jupiter, kotlinx-coroutines-test, Turbine, AssertK). * 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 * 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 - * targets AGP 8.x and we build on AGP 9.2. It isn't needed for *local* unit tests anyway -
* `com.android.build.gradle.tasks.factory.AndroidUnitTest` extends Gradle's [Test] task, so calling * `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 * `useJUnitPlatform()` on it is enough (this mirrors `DomainModuleConventionPlugin`, which does the
* same for pure-JVM modules). * same for pure-JVM modules).
@@ -21,7 +21,7 @@ class AndroidUnitTestConventionPlugin : Plugin<Project> {
dependencies { dependencies {
add("testImplementation", libs.findBundle("unit-test").get()) add("testImplementation", libs.findBundle("unit-test").get())
add("testRuntimeOnly", libs.findLibrary("junit-jupiter-engine").get()) add("testRuntimeOnly", libs.findLibrary("junit-jupiter-engine").get())
// Gradle 9 dropped the bundled launcher; JUnit 5 won't start without it. // Gradle 9 dropped the bundled launcher; the JUnit Platform won't start without it.
add("testRuntimeOnly", libs.findLibrary("junit-platform-launcher").get()) add("testRuntimeOnly", libs.findLibrary("junit-platform-launcher").get())
} }

View File

@@ -8,7 +8,7 @@ import org.gradle.kotlin.dsl.withType
/** /**
* Pure-Kotlin (JVM) module for the domain layer: no Android dependencies. Adds Coroutines (for * 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. * `Flow`-returning repository interfaces) and runs unit tests on the JUnit Platform.
*/ */
class DomainModuleConventionPlugin : Plugin<Project> { class DomainModuleConventionPlugin : Plugin<Project> {
override fun apply(target: Project) = with(target) { override fun apply(target: Project) = with(target) {
@@ -25,7 +25,7 @@ class DomainModuleConventionPlugin : Plugin<Project> {
add("testImplementation", libs.findLibrary("mockk").get()) add("testImplementation", libs.findLibrary("mockk").get())
add("testImplementation", libs.findLibrary("kotlinx-coroutines-test").get()) add("testImplementation", libs.findLibrary("kotlinx-coroutines-test").get())
add("testRuntimeOnly", libs.findLibrary("junit-jupiter-engine").get()) add("testRuntimeOnly", libs.findLibrary("junit-jupiter-engine").get())
// Gradle 9 dropped the bundled launcher; JUnit 5 won't start without it. // Gradle 9 dropped the bundled launcher; the JUnit Platform won't start without it.
add("testRuntimeOnly", libs.findLibrary("junit-platform-launcher").get()) add("testRuntimeOnly", libs.findLibrary("junit-platform-launcher").get())
} }

View File

@@ -7,7 +7,7 @@ import org.gradle.kotlin.dsl.configure
import org.gradle.kotlin.dsl.getByType import org.gradle.kotlin.dsl.getByType
import org.jetbrains.kotlin.gradle.dsl.KotlinProjectExtension import org.jetbrains.kotlin.gradle.dsl.KotlinProjectExtension
internal const val COMPILE_SDK = 36 internal const val COMPILE_SDK = 37
internal const val MIN_SDK = 24 internal const val MIN_SDK = 24
internal const val TARGET_SDK = 36 internal const val TARGET_SDK = 36
internal const val JVM_TARGET = 17 internal const val JVM_TARGET = 17

View File

@@ -18,7 +18,7 @@ import org.junit.jupiter.api.Test
/** /**
* Tests for the (thin pass-through) [GetCharactersPageUseCase]: it must forward the requested page to * Tests for the (thin pass-through) [GetCharactersPageUseCase]: it must forward the requested page to
* the repository and return its result verbatim - success and error alike. Pure JVM test on the * the repository and return its result verbatim - success and error alike. Pure JVM test on the
* JUnit 5 platform (see DomainModuleConventionPlugin); the [CharacterRepository] collaborator is a * JUnit Platform (see DomainModuleConventionPlugin); the [CharacterRepository] collaborator is a
* MockK mock, stubbed with `coEvery` and verified with `coVerify`. * MockK mock, stubbed with `coEvery` and verified with `coVerify`.
*/ */
class GetCharactersPageUseCaseTest { class GetCharactersPageUseCaseTest {

View File

@@ -1,46 +1,46 @@
[versions] [versions]
# Build / language # Build / language
agp = "9.0.1" agp = "9.2.1"
kotlin = "2.3.20" kotlin = "2.4.0"
# AndroidX - core / lifecycle / activity / views # AndroidX - core / lifecycle / activity / views
androidxCore = "1.18.0" androidxCore = "1.19.0"
androidxLifecycle = "2.10.0" androidxLifecycle = "2.10.0"
androidxActivity = "1.13.0" androidxActivity = "1.13.0"
androidxAppcompat = "1.7.0" androidxAppcompat = "1.7.1"
androidxFragment = "1.8.5" androidxFragment = "1.8.9"
androidxRecyclerview = "1.4.0" androidxRecyclerview = "1.4.0"
androidxNavigation = "2.9.0" androidxNavigation = "2.9.8"
# Compose (BOM-managed) # Compose (BOM-managed)
composeBom = "2026.03.01" composeBom = "2026.05.01"
# Async / serialization # Async / serialization
coroutines = "1.10.2" coroutines = "1.11.0"
kotlinxSerialization = "1.8.1" kotlinxSerialization = "1.11.0"
kotlinxCollectionsImmutable = "0.3.8" kotlinxCollectionsImmutable = "0.5.0"
# DI # DI
koin = "4.1.0" koin = "4.2.1"
# Networking # Networking
ktor = "3.1.3" ktor = "3.5.0"
# Image loading # Image loading
coil = "3.1.0" coil = "3.5.0"
# Logging # Logging
timber = "5.0.1" timber = "5.0.1"
# Material Components (Views renderer) # Material Components (Views renderer)
material = "1.12.0" material = "1.14.0"
# Testing # Testing
junitJupiter = "5.11.4" junitJupiter = "6.1.0"
junitPlatform = "1.11.4" junitPlatform = "6.1.0"
turbine = "1.2.0" turbine = "1.2.1"
assertk = "0.28.1" assertk = "0.28.1"
mockk = "1.14.3" mockk = "1.14.11"
androidxTestExt = "1.3.0" androidxTestExt = "1.3.0"
androidxTestRunner = "1.7.0" androidxTestRunner = "1.7.0"
androidxEspresso = "3.7.0" androidxEspresso = "3.7.0"

View File

@@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-9.1.0-bin.zip distributionUrl=https\://services.gradle.org/distributions/gradle-9.5.1-bin.zip
networkTimeout=10000 networkTimeout=10000
validateDistributionUrl=true validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME