生成された画面はエミュレータで完璧に動いていました。問題は、その画面のフォルダを自分の壁紙アプリのモジュールへコピーした瞬間に起きました。./gradlew assembleRelease は通るのに、デバッグビルドだけが Unsupported class file major version で落ちる。原因にたどり着くまで半日かかりましたが、犯人は私のコードでも生成されたコードでもなく、AI Studio が同梱してきた build.gradle.kts の中で宣言された AGP と Kotlin のバージョンでした。
AI Studio が Kotlin と Jetpack Compose のアプリをテキストから生成し、エミュレータ実行から実機転送、Google Play 内部テストまで一画面でつなげられるようになって、個人開発の試作と配布の距離はずいぶん縮みました。けれど生成物は「新規アプリ」として整っているだけで、既に動いている自分のアプリに混ぜる前提では作られていません。ここに静かな落とし穴があります。
ここでは、生成された Compose を既存アプリへ取り込むときに発生するバージョンずれを、version catalog を単一の真実として固定し、取り込み口で生成側の宣言を機械的に検査するゲートで防ぐ設計をまとめます。私が個人開発で運用している Android 壁紙アプリの実運用で踏んだ失敗と、それを止めた具体的な構成を中心にまとめます。
なぜ「動いた生成コード」が既存アプリで壊れるのか
AI Studio の生成物は、その時点で最新の安定版に寄せた依存を宣言します。一方、長く運用しているアプリは、互換性を確かめながら少しずつ上げてきた固有のバージョン構成を持っています。この二つを単純にマージすると、Gradle は依存解決の途中でより新しい方へ寄せるため、あなたが意図的に止めていたバージョンが、取り込みと同時に黙って引き上げられます。
実際に私が遭遇した壊れ方を、影響範囲とともに整理します。
| ずれた要素 | 生成側が宣言しがちな値 | 既存アプリで起きたこと |
|---|---|---|
| AGP(Android Gradle Plugin) | 最新メジャー(例: 9.x 系) | Java 8 API を使う旧ライブラリで NoClassDefFoundError。desugaring の有効化が前提に |
| Kotlin / Compose コンパイラ | 最新安定 | Compose コンパイラと Kotlin の対応表からずれ、Compose Compiler requires Kotlin version で停止 |
| core ライブラリ(画像・DI 等) | 最新 | API シグネチャ変更で既存画面側がコンパイルエラー |
| minSdk / targetSdk | 生成時点の推奨 | minSdk が上がり、サポート対象だった旧端末が配信対象から外れる |
このうち AGP の引き上げは特に厄介でした。私のアプリでは画像表示ライブラリが内部で Java 8 の Supplier を参照していて、AGP のメジャー更新後に Android 6.0.1 帯の端末で起動直後にクラッシュするようになったのです。解決自体は一行で、coreLibraryDesugaringEnabled を有効化して desugaring ライブラリを足すだけでしたが、「生成コードを混ぜたらクラッシュ率が上がった」という因果が見えにくいのが本当の問題でした。この desugaring 周りの詳しい挙動は古い端末で起きる Java 8 由来のクラッシュを desugaring で止めた記録に切り分けの手順を書いています。
ここで効くのが「単一の真実(single source of truth)」という考え方です。バージョンを各モジュールの build.gradle.kts に散らさず、gradle/libs.versions.toml の version catalog 一箇所に集約しておけば、生成物が独自に宣言したバージョンは取り込みの時点で必ず catalog と突き合わせられるようになります。
version catalog を単一の真実として固定する
まず、プロジェクト全体のバージョンを version catalog に寄せます。生成物が個別に書いてくる implementation("androidx.compose...:1.x.x") のような直書きを排除し、すべて catalog 参照に統一するのが出発点です。
# gradle/libs.versions.toml — プロジェクトで唯一バージョンを宣言する場所
[versions]
agp = "9.0.1" # ここで止める。生成物が 9.1.x を宣言しても上げない
kotlin = "2.2.20"
composeCompiler = "2.2.20" # kotlin と一致させる(Kotlin 2.x は composeOptions 不要だが明示管理)
composeBom = "2026.05.01"
coreDesugar = "2.1.5" # AGP メジャー更新後の Java 8 由来クラッシュ対策
minSdk = "23" # Android 6.0。生成物が上げてきても維持する
targetSdk = "36"
[libraries]
compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" }
compose-ui = { group = "androidx.compose.ui", name = "ui" }
compose-material3 = { group = "androidx.compose.material3", name = "material3" }
desugar-jdk-libs = { group = "com.android.tools", name = "desugar_jdk_libs", version.ref = "coreDesugar" }
[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }次に、モジュール側ではバージョンを一切書かず、catalog の別名だけを参照します。Compose は BOM 経由で版を揃えるので、個々のライブラリにバージョンを付けないのが要点です。
// app/build.gradle.kts — バージョンは書かない。catalog の別名だけを使う
plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.android)
alias(libs.plugins.kotlin.compose)
}
android {
compileSdk = libs.versions.targetSdk.get().toInt()
defaultConfig {
minSdk = libs.versions.minSdk.get().toInt()
targetSdk = libs.versions.targetSdk.get().toInt()
}
compileOptions {
// AGP メジャー更新で古い端末が落ちないよう desugaring を常時有効化
isCoreLibraryDesugaringEnabled = true
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
}
dependencies {
val composeBom = platform(libs.compose.bom)
implementation(composeBom)
implementation(libs.compose.ui)
implementation(libs.compose.material3)
coreLibraryDesugaring(libs.desugar.jdk.libs) // 上の TOML と対で必ず入れる
}これで「どのモジュールから見てもバージョンは catalog の一箇所」という状態になります。生成物をこのプロジェクトに混ぜるとき、生成側が直書きしたバージョンはそのままでは catalog 経由にならないので、取り込み口でそれを検出できます。