Beautiful HD Wallpapers v2.0.0 をリリースした 7 日後、Firebase Crashlytics で「早期クラッシュ」タグの付いた赤い Issue が立ち上がりました。NoClassDefFoundError: java.util.function.Supplier。発生端末を絞り込んでみたところ、Android 6.0.1(API 23)のユーザーが起動して 3 秒以内に全員落ちている、という現象でした。7 日で 12 events / 4 users、しかも 4 人とも別ユーザーです。
minSdk 23 を支えているデバイスは、累計 5,000 万 DL を超えた個人開発キャリアの中で、もう何年も「最後の数 % のためだけにメンテし続ける層」になっていました。それでも、起動 3 秒で落ちるのは見過ごせません。クラッシュログから Claude Code に状況を渡して、build.gradle.kts の 1 行で根治するまでの経緯を、同じ落とし穴を踏みかけている方のために残しておきます。
クラッシュの正体 — Java 8 Supplier が API 23 では見つからない
スタックトレースを最初に見たとき、私が拾ったのは次の 3 行でした。
Fatal Exception: java.lang.NoClassDefFoundError:
Failed resolution of: Ljava/util/function/Supplier;
at com.bumptech.glide.GlideBuilder.<init>(GlideBuilder.java:64)
java.util.function.Supplier は Java 8 で追加された関数型インターフェースです。Android では API 24(Nougat)以降のランタイムにしか入っていません。API 23 は Marshmallow 後半なので、ここから漏れます。
公式ドキュメント上、Glide 5.x の minSdk は 21 となっていますが、内部実装に Java 8 API が混ざるようになったのは Glide 5.0.0 以降です。AGP(Android Gradle Plugin)9.x にバージョンアップしたタイミングで、Glide が内部で Supplier を直接参照する経路が増えていました。
ここで効くのが Core Library Desugaring です。R8 が、Java 8 以降の API を低 API レベルの端末でも動かせるように「降ろし直し(desugar)」してくれる仕組みです。私のプロジェクトでは、依存自体は宣言済みだったのに、有効化フラグだけが立っていませんでした。
なぜ依存追加だけでは足りないのか — desugar の二段構造
build.gradle.kts の dependencies ブロックに coreLibraryDesugaring で desugar_jdk_libs を追加していても、R8 はそれを参照しません。compileOptions 側に明示的なフラグが必要です。これは「依存があるか」と「実際に使うか」を別管理にしている AGP の設計です。
私の場合は v2.0.0 までは Glide 4.x のままで、Java 8 API への依存が静的に表に出ていなかったため、フラグなしでも通っていました。v2.0.0 で AGP 9.x + Glide 5.0.5 に上げた瞬間に表面化した、というのが Claude Code との対話で見えてきた背景です。
12 年間個人で Android / iOS を書いてきて学んだのは、依存ツリーの「使われ方が変わる」場面は常にライブラリのメジャーアップ時にやってくる、ということです。Glide 4 → 5 のように API 表面が大きく変わらないアップデートでも、内部の Java バージョン依存は静かに動きます。
Claude Code に渡した最小プロンプト
私は Claude Code に「Crashlytics の Issue 全文 + build.gradle.kts + ライブラリ更新 PR の diff」だけを渡しました。試行錯誤の余白を残すために、解決方針は与えませんでした。
私が手元に置いている、Crashlytics 系のクラッシュを Claude Code に流すときのテンプレートです。
## Context
- App: Beautiful HD Wallpapers (Android, minSdk 23, targetSdk 36)
- AGP: 9.0.0, Kotlin: 2.1.21, Java: 17
- Crashlytics Issue (7d): NoClassDefFoundError on java.util.function.Supplier
- Affected devices: all Android 6.0.1 (API 23) users (4 / 4)
- Reproduces in: 3 seconds of cold start on emulator (API 23)
## Recent change (suspect)
- Glide: 4.16.0 -> 5.0.5
- AGP: 8.11.1 -> 9.0.0
## Ask
- 原因の仮説を 3 つ出してください
- それぞれの確認方法(具体コマンドや grep)を含めてください
- 最終的な修正は build.gradle.kts のどの行が変わるかを diff 形式で示してください
ポイントは、Ask の 1 行目に「仮説を 3 つ」と置いていることです。これがないと、Claude Code は最尤の仮説に飛びついて他の経路を見落とすことがあります。仮説 3 本立てを強制すると、調査の網が広がります。
1 行追加で根治するパッチ
Claude Code が最終的に絞り込んだ diff は、build.gradle.kts の compileOptions ブロックに 1 行追加するだけでした。
android {
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
// 追加: R8 に Java 8 API を低 API へ降ろす指示を出す
isCoreLibraryDesugaringEnabled = true
}
}
dependencies {
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.1.5")
implementation("com.github.bumptech.glide:glide:5.0.5")
}
isCoreLibraryDesugaringEnabled = true の 1 行が、依存と R8 の二段構造を繋ぎます。Groovy DSL の場合は coreLibraryDesugaringEnabled true です。
ローカルで API 23 のエミュレータを立てて、起動 → カテゴリ画面 → 詳細画面 → 戻る、までを 3 周しても落ちなくなったところで、段階公開を 5% から始めました。
Crashlytics で「直った」を確認するまでの監視
私が個人開発で重視しているのは「直ったかどうか」を Crash-free users で確認することです。Issue 単発の終息ではなく、対象クラスタの母数がそもそも増えていないかも見ます。
fatal_issue_id == "<issue id>"
AND app_version == "2.1.0"
AND os.display_version starts_with "6.0"
このフィルタで Crashlytics の Issue ページを見て、48 時間で 0 events になっていることを確認しました。Crash-free users は v2.0.0 の 99.3% から v2.1.0 で 99.78% に戻り、ANR < 0.20% も維持できています。AdMob のリワード広告 fill rate にも副作用が出ていないことを確認した上で、段階公開を 25% → 50% → 100% と進めました。
公式ドキュメントには書かれていませんが、Crash-free users が「直前バージョンより悪化した」かどうかだけを見るのではなく、Android 6.0.x だけに絞った Crash-free を別途出すと、minSdk 付近で起きる潜在的な問題を早く拾えます。Crashlytics のフィルタを保存してダッシュボードに固定しておくのがお勧めです。
同じ落とし穴を踏まないための CI チェック
このタイプの問題は、「依存と compileOptions のどちらが正で、どちらが従なのか」を曖昧にしたままバージョンを上げると再発します。私の対処は、Claude Code に「PR テンプレート + 静的チェックの素案」を一緒に作らせることでした。
CI で実行している簡単な grep ベースのチェック例です。
#!/usr/bin/env bash
set -euo pipefail
GRADLE_FILE="app/build.gradle.kts"
if grep -q "coreLibraryDesugaring(" "$GRADLE_FILE"; then
if ! grep -q "isCoreLibraryDesugaringEnabled\s*=\s*true" "$GRADLE_FILE"; then
echo "❌ desugar_jdk_libs を依存しているのに compileOptions が false です"
exit 1
fi
fi
echo "✅ desugar 設定の整合性 OK"
Glide や Coil、OkHttp などの主要 ImageLoader / HTTP クライアントは、メジャーアップのたびに Java 標準ライブラリの新しい API を内部で触り始めます。CI 段階でこの 1 行をチェックしておくと、minSdk 23 を保持し続ける個人開発プロジェクトでも、Java 8 API 起因の起動クラッシュをほぼ予防できます。
個人開発 12 年で学んだ依存ツリーとの付き合い方
両親の世代の宮大工が、見えない接合部にこそ時間をかけていた話を、祖父から繰り返し聞いて育ちました。Android の build.gradle.kts も、表に出てこない compileOptions や coreLibraryDesugaring こそが、長く動き続けるアプリの接合部だと考えています。
1997 年に独学でインターネットに触れた頃から、外向きの華やかさよりも、内側で静かに支える設定や宣言が好きでした。Beautiful HD Wallpapers が累計 5,000 万 DL を超えてもなお現役で動いているのは、年に何度か、こういう 1 行を Claude Code と一緒に拾い直す時間を取っているからです。
派手なリファクタリングよりも、月に 1 回、Glide や AGP の changelog と build.gradle.kts を並べて Claude Code に読ませる時間を取る方が、結果としてユーザーのクラッシュを減らします。私はこの方法を「依存の棚卸し」と呼んでいて、リリース直前ではなく、リリース後 1〜2 週間の落ち着いた時期に行うことを推奨しています。
次に試してほしいこと
もしご自身の Android プロジェクトで Glide 5.x または Coil 2.x を使っていて、minSdk が 24 未満であれば、まず app/build.gradle.kts の compileOptions ブロックに isCoreLibraryDesugaringEnabled = true が立っているかだけを確認してみてください。これが立っていなければ、その 1 行を追加して、API 23 のエミュレータでコールドスタートを 3 回ほど試すところから始めるのがお勧めです。
同じような落とし穴に取り組んでいる方の参考になれば幸いです。お読みいただきありがとうございました。