[versions] # Build tooling. Pinned, never floating. agp and kotlin are coupled (below); ksp is # staged for a later phase and unapplied today -- KSP2 versions independently of Kotlin. # # AGP 9 has BUILT-IN Kotlin: it compiles Kotlin itself rather than delegating to the # org.jetbrains.kotlin.android plugin, which is incompatible with its DSL and fails the # build if applied. DO NOT add it. # # By default AGP compiles with the KGP it bundles -- 2.2.10 for AGP 9.3.1, per its POM -- # and that version caps jvmTarget at 24, which would cap the app's bytecode below the JDK # everything else runs on. The root build.gradle.kts therefore puts KGP on the buildscript # classpath explicitly, which AGP's built-in Kotlin picks up instead. That is the only # reason `kotlin` here can be ahead of what AGP ships. # # The Compose compiler plugin is versioned in lockstep with Kotlin and is read from this # same `kotlin` entry, so the two cannot drift. Raising `kotlin` means checking that a # matching compose-compiler-gradle-plugin exists. agp = "9.3.1" kotlin = "2.4.10" ksp = "2.3.11" # AndroidX / Compose -- floating on minor + patch. The prerelease guard in # app/build.gradle.kts is what keeps `+` from selecting an alpha: several of these # (lifecycle, navigation, work, datastore, annotation) publish alphas and RCs with # version numbers ABOVE their newest stable, and Gradle's `+` would take them. # Bare `+`, not "2026.+": the year is the major in this scheme (YYYY.MM.PP), so a # 2026-prefixed float would quietly stop finding releases on 1 January and keep # building green against a frozen BOM. composeBom = "+" coreKtx = "1.+" activityCompose = "1.+" lifecycle = "2.+" navigation = "2.+" work = "2.+" datastore = "1.+" media3 = "1.+" room = "2.+" documentfile = "1.+" annotation = "1.+" # Test -- floating, same rules as above. junit = "4.+" androidxJunit = "1.+" espressoCore = "3.+" # PINNED, unlike its neighbours. Under semver a 0.x minor is allowed to break, and # this library is load-bearing exactly where breakage is hardest to see: the wrapper # reaches for smartexception.java.Exceptions only when an FFmpeg call FAILS, so a # moved class surfaces as an R8 missing-class error at release time, or as a crash on # the error path -- the least-exercised code in the app. Bump it deliberately. smartException = "0.2.1" # Lint/format. PINNED, deliberately, while the libraries above float. # # A library bump that misbehaves usually still compiles. An analysis-tool bump does # something worse: a new rule in ktlint or detekt makes files nobody touched stop # passing, so CI goes red on a PR whose diff cannot explain it. Upgrading these is # therefore an act with its own commit -- run the tool, read the new findings, fix or # relax them -- which is exactly the reviewable step floating is meant to skip. # # ktlint owns formatting; detekt owns static analysis (its formatting ruleset stays # off, so the two can never disagree about the same line). # detekt 2.0 is the only line with Gradle 9 support -- stable 1.23.x tops out at # Gradle 8.12, and this project is on 9.7.1. ktlint = "14.2.0" detekt = "2.0.0-alpha.6" # JaCoCo coverage agent. Pinned rather than inheriting whatever Gradle 9.7.1 bundles, # so the agent version that reads Kotlin 2.2.10 bytecode is stated, not implied. jacoco = "0.8.15" # Robolectric. PINNED, and it belongs with ktlint/detekt/jacoco above rather than with # the floating libraries, for two reasons that both point the same way. # # First, the prerelease guard in app/build.gradle.kts only covers the groups this project # floats -- "androidx.", "junit", "com.arthenica" -- so org.robolectric is unguarded, and # a "4.+" here would resolve straight to 4.17-beta-3, which is the newest thing published. # Second, Robolectric is not a library the app ships: it is the JVM's Android runtime, and # a version bump changes which android-all jar the tests execute against. That is the same # "a tool moved under a diff that cannot explain it" failure the linters are pinned for. # # 4.16.1 is the newest RELEASED version; the 4.17 line is beta-only at the time of writing. robolectric = "4.16.1" # kotlinx-coroutines-test. Already on the unit-test classpath transitively, through # compose-ui-test-junit4 -- declared here because a source file now imports it, and a direct # import of a transitive is a dependency nobody chose. # # PINNED, for the same reason as robolectric above: org.jetbrains.kotlinx is not one of the # groups in the prerelease guard's `floatedGroupPrefixes`, so a "1.+" here would be free to # resolve to a milestone build. This value is what the Compose BOM already resolves it to, so # stating it changes nothing in the graph today; if the BOM moves ahead, Gradle takes the # higher version and this stays a floor rather than a conflict. coroutinesTest = "1.9.0" [libraries] androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" } androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycle" } androidx-lifecycle-runtime-compose = { group = "androidx.lifecycle", name = "lifecycle-runtime-compose", version.ref = "lifecycle" } androidx-lifecycle-viewmodel-compose = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-compose", version.ref = "lifecycle" } # Compose — versions come from the BOM, so no version.ref here. compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" } compose-material3 = { group = "androidx.compose.material3", name = "material3" } compose-material3-windowsize = { group = "androidx.compose.material3", name = "material3-window-size-class" } compose-material-icons-extended = { group = "androidx.compose.material", name = "material-icons-extended" } compose-ui = { group = "androidx.compose.ui", name = "ui" } compose-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics" } compose-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" } compose-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" } compose-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" } compose-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" } androidx-navigation-compose = { group = "androidx.navigation", name = "navigation-compose", version.ref = "navigation" } androidx-work-runtime-ktx = { group = "androidx.work", name = "work-runtime-ktx", version.ref = "work" } androidx-work-testing = { group = "androidx.work", name = "work-testing", version.ref = "work" } androidx-datastore-preferences = { group = "androidx.datastore", name = "datastore-preferences", version.ref = "datastore" } androidx-documentfile = { group = "androidx.documentfile", name = "documentfile", version.ref = "documentfile" } androidx-annotation = { group = "androidx.annotation", name = "annotation", version.ref = "annotation" } # Media3 — the hardware fast path (Apache-2.0). media3-transformer = { group = "androidx.media3", name = "media3-transformer", version.ref = "media3" } media3-effect = { group = "androidx.media3", name = "media3-effect", version.ref = "media3" } media3-common = { group = "androidx.media3", name = "media3-common", version.ref = "media3" } media3-exoplayer = { group = "androidx.media3", name = "media3-exoplayer", version.ref = "media3" } media3-muxer = { group = "androidx.media3", name = "media3-muxer", version.ref = "media3" } # Room — job history. Added in a later phase; KSP plugin stays unapplied until then. androidx-room-runtime = { group = "androidx.room", name = "room-runtime", version.ref = "room" } androidx-room-ktx = { group = "androidx.room", name = "room-ktx", version.ref = "room" } androidx-room-compiler = { group = "androidx.room", name = "room-compiler", version.ref = "room" } # Required at runtime by the ffmpeg-kit-next wrapper: AbstractSession.fail() # references smartexception.java.Exceptions. Debug builds tolerate its absence through # lazy class loading, so this only surfaces as a crash on the first FFmpeg error -- # or, as it did here, as an R8 missing-class error. smart-exception-java = { group = "com.arthenica", name = "smart-exception-java", version.ref = "smartException" } junit = { group = "junit", name = "junit", version.ref = "junit" } androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "androidxJunit" } androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" } # Robolectric — an Android runtime for the JVM test source set, so file-lifecycle behaviour # that needs a real Context can be verified without a device. The instrumented suite cannot # run on the development host at all (see CLAUDE.md), so an androidTest-only red test is not # a TDD loop anyone here can execute. robolectric = { group = "org.robolectric", name = "robolectric", version.ref = "robolectric" } # Only for its `runTest`, and only to drain the collector kotlinx-coroutines-test installs # process-wide. See EscapedCoroutineErrors.kt in the JVM test source set. kotlinx-coroutines-test = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-test", version.ref = "coroutinesTest" } [plugins] # com.android.application and org.jetbrains.kotlin.plugin.compose are deliberately absent. # They come from the root buildscript classpath (see build.gradle.kts) so that a newer KGP # can override AGP's bundled one, and the module applies them by id(). An alias here would # be dead weight that reads like the source of truth. # # ktlint and detekt are ordinary plugin-portal resolutions and stay aliases. ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" } ktlint = { id = "org.jlleitschuh.gradle.ktlint", version.ref = "ktlint" } detekt = { id = "dev.detekt", version.ref = "detekt" } # DO NOT add org.jetbrains.kotlin.android — AGP 9 built-in Kotlin makes it a build failure.