2026年9月11日金曜日

AGP 9 にしたら ML Kit のバーコードスキャンがリリースビルドでクラッシュするようになった

ユーザーからバーコード読み取り画面を開こうとするとクラッシュするという問い合わせがありました。手元で確認すると、リリースビルドでのみクラッシュし、デバッグビルドでは再現しませんでした。

スタックトレースを確認すると次のようになっていました。 java.lang.NullPointerException: Attempt to read from field '...zzi zzg.zza' on a null object reference at com.google.mlkit.vision.barcode.BarcodeScanning.getClient(play-services-mlkit-barcode-scanning@@18.3.1:3) at ... ML Kit の BarcodeScanning.getClient() の中で NullPointerException になっています。
R8 をかけたビルドでのみ発生するので、R8 が削除したものの一覧である app/build/outputs/mapping/release/usage.txt を見ると、BarcodeRegistrar の引数なしコンストラクタが削除されていました。 com.google.mlkit.vision.barcode.internal.BarcodeRegistrar: public void <init>() BarcodeRegistrar は ComponentRegistrar の実装クラスです。Firebase Components は AndroidManifest.xml の meta-data に書かれたクラス名から Class.forName し、引数なしコンストラクタで ComponentRegistrar の実装クラスを生成します。
コンストラクタが削除されるとレジストラを生成できず、barcode のコンポーネントが登録されないまま getClient() が呼ばれて NullPointerException になります。

firebase-components 16.1.0 に同梱されている keep rule は以下です。 -keep class * implements com.google.firebase.components.ComponentRegistrar メンバーの指定がないため、クラスは残りますがコンストラクタは残りません。

このルールは変わっていないのに以前のリリースビルドでは問題が起きていませんでした。 R8 のバージョンが変わった、つまり AGP の更新が原因ではないかと考え、AGP を 9.3.2 と 8.13.2 で切り替えてリリースビルドを比較しました。

firebase-components のバージョンも適用されるルールも同じですが、AGP 8.13.2 では BarcodeRegistrar.<init>() が残り、AGP 9.3.2 では削除されていました。なお android.enableR8.fullMode は設定したことがなく、full mode は AGP 8.0 からデフォルトでオンなので今回は関係ありません。

app の proguard-rules.pro に引数なしコンストラクタを明示した keep rule を追加しました。 -keep class * implements com.google.firebase.components.ComponentRegistrar { <init>(); } firebase-components は 19.0.0 で同じ内容にルールが修正されています。ただし com.google.mlkit:barcode-scanning は 17.3.0 が最新で、そこから引かれる firebase-components は 16.1.0 のため、ML Kit が新しい firebase-components を引くようになるまではアプリ側で補う必要があります。

修正後のリリースビルドでは usage.txt からコンストラクタの削除が消え、バーコードスキャン画面が正常に開くようになりました。

同じように AndroidManifest.xml の meta-data からリフレクションで生成される androidx.startup の Initializer について確認しましたが、startup-runtime 1.2.0 の consumer rules には -keepnames class * extends androidx.startup.Initializer に加えて引数なしコンストラクタを明示した -keep class * extends androidx.startup.Initializer { <init>(); } が含まれており、usage.txt でもコンストラクタは残っていました。



2026年7月6日月曜日

androidx の core-ktx の内容は core に含まれるようになった

core 1.19.0-alpha01 から core-ktx API は core library に統合されて、すべての Kotlin 拡張機能はメインの core artifact の一部になりました。 互換性のために core-ktx は空のアーティファクトとして残されていますが、空のアーティファクトを含める意味もないので早めに core に変更しておきましょう。 - androidx-core-ktx = { module = "androidx.core:core-ktx", version.ref = "androidxCore" } + androidx-core = { module = "androidx.core:core", version.ref = "androidxCore" }

2026年6月8日月曜日

onSubscription で load を開始するとはまる問題

class ItemListViewModel(...) : ViewModel() { val uiState: StateFlow<UiState> = baseFlow .onSubscription { loadInitialPage() } .stateIn( scope = viewModelScope, started = SharingStarted.WhileSubscribed(5000), initialValue = UiState.Initial ) このコードで取得した ItemList を ItemListScreen で表示している。ItemList にはページングがあり、loadInitialPage() は初期ページを読み込む処理である。

この画面から別の画面、例えば ItemDetailScreen に行って戻ってくる場合、5秒以上経ってから戻ってくると onSubscription { } が再度呼ばれる。そのため、スクロールして2ページ目以降を読み込んで表示している場合、5秒以上経ってから戻ってくると1ページ目に戻されてしまう。

val uiState: StateFlow<UiState> = baseFlow .onSubscription { if (uiState.value == UiState.Initial) { loadInitialPage() } } .stateIn( scope = viewModelScope, started = SharingStarted.WhileSubscribed(5000), initialValue = UiState.Initial ) このように状態チェックを入れればいいのだが、それなら init { loadInitialPage() } でいいじゃん。と思うのである。私は init 派である。

(実際これで問題になっていたコードを直したので、実装時に気をつけることが少ないほうがいいと思うんだ)