2026年版 AndroidのIn-App Update API:フレキシブルか即時か、いつ強制すべきか
GoogleのIn-App Update APIの実践ガイド。フレキシブルフローと即時フローの違い、更新優先度、経過日数、そして誰もが忘れがちな再開チェックについて解説する。
Play Storeの自動更新は遅かれ早かれユーザーの大半をカバーするが、その「遅かれ早かれ」がこの文の大部分を占めている。Wi-Fi限定の設定、ストレージ不足、あるいは単にPlay Storeアプリを開かないユーザーによって、誰かが3バージョン遅れたままになることがある——2週間前に直したバグ入りのビルドをまだ使っていたり、さらに悪ければ、バックエンドがもうサポートしていないサーバー契約にまだ衝突していたりする。GoogleのIn-App Update APIはそのギャップのために存在する。アプリが新しいバージョンの有無を確認し、ユーザーをどこにも移動させずにアプリ内から更新を促せるようにするものだ。
このAPIには2つの異なるフローがあり、間違った方を選ぶことがこの機能を下手に実装してしまう最も一般的な原因だ。
実際の仕組み
このAPIはPlay Core(com.google.android.play:app-update-ktx)の一部だ。AppUpdateManagerに現在のAppUpdateInfoを要求すると、更新が利用可能かどうか、そしてGoogleがその更新について何を把握しているかがわかる。
val updateManager = AppUpdateManagerFactory.create(context)
updateManager.appUpdateInfo.addOnSuccessListener { info ->
if (info.updateAvailability() == UpdateAvailability.UPDATE_AVAILABLE) {
// decide which flow to start based on info.updatePriority()
// and info.clientVersionStalenessDays()
}
}
このAppUpdateInfoオブジェクトが実質的にAPIのすべてだ——あとはそれをどう扱うかを決めるだけである。
フレキシブルと即時:異なる2つの役割
フレキシブルは、ユーザーがアプリを使い続けている間にバックグラウンドで更新をダウンロードし、準備ができると「再起動して更新」という小さなスナックバーを表示する。ユーザーはブロックされず、プロンプトを無視することもできる。緊急性のないもの——UIの微調整、新機能、破壊的でないバグ修正——には、これがデフォルトとして適切だ。
if (info.isUpdateTypeAllowed(AppUpdateType.FLEXIBLE)) {
updateManager.startUpdateFlowForResult(
info, activityResultLauncher, AppUpdateOptions.newBuilder(AppUpdateType.FLEXIBLE).build()
)
}
ダウンロードが完了すると、InstallStateUpdatedListenerがInstallStatus.DOWNLOADEDを報告し、インストールの完了を明示的に呼び出す。
updateManager.registerListener { state ->
if (state.installStatus() == InstallStatus.DOWNLOADED) {
updateManager.completeUpdate() // shows the snackbar's restart action
}
}
即時はその逆だ。更新がインストールされるまでユーザーがアプリを使えなくする、全画面のブロッキングフローである。設計上、破壊的であることを意図しており、それゆえ本当に一つの状況だけのために存在する——現在のバージョンが重大な形で壊れている場合だ。セキュリティ修正、古いクライアントを壊してしまうRoomのスキーマ移行、古いビルドがリクエストのたびにクラッシュするようになるバックエンドAPIの変更。誰かが古いバージョンをもう1日使い続けることが本当に有害だと説明できないなら、それは即時更新ではなく、フレキシブル更新にすべきだ。
if (info.isUpdateTypeAllowed(AppUpdateType.IMMEDIATE)) {
updateManager.startUpdateFlowForResult(
info, activityResultLauncher, AppUpdateOptions.newBuilder(AppUpdateType.IMMEDIATE).build()
)
}
私がこれを使ったのは一度だけで、Granynで定期取引の保存方法を変更したリリースのときだった——古いクライアントが新しいスキーマに書き込むと、残高がサイレントに誤計算されてしまう状況だった。これはブロックする価値がある。ボタンの名前変更程度では、そうではない。
優先度(と経過日数)の判断
AppUpdateInfoは、フレキシブル/即時の選択を勘に任せる代わりに、2つのシグナルを与えてくれる。
updatePriority()— Play Consoleでリリース展開時に、リリースごとに自分で設定する0〜5の整数値。差分から自動計算されるものではなく、このリリースがどれほど緊急かをGoogle(および自分のクライアントコード)に伝える値だ。clientVersionStalenessDays()— この特定のユーザーに対してその更新が利用可能になってから何日経ったかを示す値で、これは自分がいつリリースしたかとは異なる。段階的ロールアウトでは、同じリリースであってもインストールベース全体でこの数値がばらつく。
私にとってうまく機能しているパターンはこうだ。優先度4〜5を即時対象として扱い、それ以外は優先度と経過日数を組み合わせる——優先度3のリリースは、1〜2週間放置されて初めて即時扱いになり、割り込む前にユーザーに猶予期間を与える。
val shouldForce = info.updatePriority() >= 4 ||
(info.updatePriority() == 3 && (info.clientVersionStalenessDays() ?: 0) > 14)
みんなが忘れがちなチェック:中断した更新の再開
即時更新は中断されうる——ユーザーがアプリをバックグラウンドに送る、プロセスが終了する、電話がかかってくる。そうなったとき、Play Coreは黙って再開してくれるわけではない。関連する画面が再開するたびに、起動時の一度きりではなく、中断した更新がないかをチェックする必要がある。
override fun onResume() {
super.onResume()
updateManager.appUpdateInfo.addOnSuccessListener { info ->
if (info.updateAvailability() == UpdateAvailability.DEVELOPER_TRIGGERED_UPDATE_IN_PROGRESS) {
updateManager.startUpdateFlowForResult(
info, activityResultLauncher, AppUpdateOptions.newBuilder(AppUpdateType.IMMEDIATE).build()
)
}
}
}
これを省略すると、このAPIの評判を悪くしている典型的な失敗パターンに陥る。ユーザーが更新の途中で中断され、メッセージに返信するためアプリをバックグラウンドに送り、戻ってくると、アプリは……なぜか動く。半分だけ更新された、一度もテストしていない状態でだ。onResumeのチェックはわずか3行だが、堅牢な即時更新と、再現できない断続的なバグ報告との差を生む。
本番ロールアウトを待たずにテストする
自分の端末で、現在インストールされている自分のビルドに対して実際の更新をトリガーすることはできない——Play Coreは自分のアカウントから見える実際のバージョン差を必要とする。実践的な構成は内部テストトラックだ。そこから古いバージョンコードをインストールし、新しいものを公開して、APIに実際の差分を見せる。GoogleはAppUpdateInfoのレスポンスをローカルで偽装できるアプリ内更新テストAPIも提供している。この機能に頻繁に触るなら、デバッグメニューに組み込む価値がある——優先度/経過日数のロジック自体を反復開発する際、実際のトラックを往復するよりずっと速い。
この機能全体を、頼りにすべきUXパターンとしてではなく、セーフティネットとして扱うべきだ。このAPIの最良のバージョンとは、ユーザーが一度も目にすることのないものだ。なぜなら自動更新がすでに仕事を終えているからで、フレキシブルと即時は、それがうまくいかなかった日のために存在している。
// 関連記事
ジャーナルの他の記事
2026年のandroidx.startup: ContentProviderの山を積まずにライブラリ初期化の順序を決める
AndroidのApp Startupライブラリの実践ガイド — initializerを1つのContentProviderに統合し、依存関係を宣言し、それでも置き換えられない遅延初期化のケースを解説する。
2026年のCameraX:カメラセッションをリークさせずにPreviewとImageAnalysisを束縛する
AndroidのCameraX実践ガイド。PreviewとImageAnalysisをライフサイクルに束縛する方法、正しいbackpressure戦略、そして回転が引き起こすクラッシュについて。
2026年版 Android Photo Picker API:ギャラリー全体へのアクセス権を求めずに写真を1枚選ばせる
AndroidのPhoto Picker APIの実践ガイド。単一・複数選択、MIMEフィルタリング、Android 13未満向けのバックポート、そしてREAD_MEDIA_IMAGESより優れている理由を解説する。