Play Console を開いて最初に目に入ったのは、伸びているアプリの数字ではありませんでした。動いていない行のほうです。
しばらく手を入れていないアプリの新規インストールが、前の月より静かになっていました。レビューが荒れたわけでも、ランキングが落ちたわけでもありません。原因は、こちらが何もしなかったことのほうにありました。
個人開発でアプリを何本か抱えていると、更新の頻度には必ず差が出ます。毎月触るものと、年に一度も触らないものが同居する。そして年次のプラットフォーム要件は、その両方に対して等しくやってきます。
厄介なのは、「更新しないアプリには関係がない」という読み方が、半分しか正しくないことでした。
提出の基準と、表示され続ける基準は一段ずれています
Google Play の対象 API レベル要件には、性質の違う二つのルールが同居しています。混ざったまま読むと判断を誤ります。
ルール 効く相手 2026-08-31 以降の水準 満たさないとどうなるか
提出のルール 新規アプリの提出と、既存アプリの更新 API 36(Android 16)以上 公開できない。ビルドをアップロードしても審査に進まない
可視性のルール すでに公開されているアプリ全部 API 35 以上 ストアからは消えないが、より新しい Android を積んだ端末の新規ユーザーに表示されなくなる
水準が一段ずれている点が要です。更新を出さないつもりなら、36 まで上げる必要はありません。ただし 35 には届いている必要があります。
つまり判断は二択ではなく三択です。「36 に上げて更新を続ける」「35 で据え置いて表示だけ守る」「どちらもせず、新規ユーザーへの露出を諦める」。
なお Wear OS と Android Automotive OS 向けのアプリは、この要件の対象外です。手元に該当するものがあれば、棚卸しの一覧から先に外しておくと迷いが減ります。
要件の一次情報は Android Developers の対象 API レベル要件 にまとまっています。年ごとに水準が動くので、記憶ではなく毎年ここを見に行くほうが確実です。
棚卸しの初手は、手元の成果物から実効値を並べることです
Play Console を1本ずつ開いて確認する方法は、アプリが3本を超えたあたりから破綻します。私はローカルに残っているビルド成果物から機械的に並べる形に変えました。
以下は AAB と APK の両方を走査して、パッケージ名・versionCode・targetSdk をタブ区切りで出すスクリプトです。
#!/usr/bin/env bash
# 手元のビルド成果物から実効 targetSdk を一覧にする
# 使い方: ./target-sdk-inventory.sh ~/builds
# 事前に bundletool(AAB 用)と aapt2(APK 用)が PATH にあること
set -euo pipefail
ROOT = " ${1 :- . } "
# bundletool を jar で持っている場合は次のように渡す:
# BUNDLETOOL="java -jar $HOME/tools/bundletool-all.jar" ./target-sdk-inventory.sh ~/builds
BUNDLETOOL = "${ BUNDLETOOL :- bundletool }"
AAPT2 = "${ AAPT2 :- aapt2 }"
printf 'artifact\tpackage\tversionCode\ttargetSdk\n'
# -print0 と read -r -d '' の組み合わせで、空白を含むパスでも壊れないようにする
find " $ROOT " -type f \( -name '*.aab' -o -name '*.apk' \) -print0 |
while IFS = read -r -d '' f ; do
pkg = "" ; vc = "" ; tsdk = ""
case " $f " in
* .aab )
# set -e があるため、読めない成果物で全体を止めないよう || true で受ける
manifest = "$( $BUNDLETOOL dump manifest --bundle = " $f " 2 >/dev/null || true )"
if [ -z " $manifest " ]; then
printf '%s\t-\t-\tREAD_FAILED\n' "$( basename " $f ")"
continue
fi
pkg = $( printf '%s' " $manifest " | sed -n 's/.*package="\([^"]*\)".*/\1/p' | head -1 )
vc = $( printf '%s' " $manifest " | sed -n 's/.*android:versionCode="\([^"]*\)".*/\1/p' | head -1 )
tsdk = $( printf '%s' " $manifest " | sed -n 's/.*android:targetSdkVersion="\([^"]*\)".*/\1/p' | head -1 )
;;
* .apk )
badging = "$( $AAPT2 dump badging " $f " 2> /dev/null || true )"
if [ -z " $badging " ]; then
printf '%s\t-\t-\tREAD_FAILED\n' "$( basename " $f ")"
continue
fi
pkg = $( printf '%s' " $badging " | sed -n "s/^package: name='\([^']*\)'.*/\1/p" )
vc = $( printf '%s' " $badging " | sed -n "s/.*versionCode='\([^']*\)'.*/\1/p" | head -1 )
tsdk = $( printf '%s' " $badging " | sed -n "s/^targetSdkVersion:'\([^']*\)'.*/\1/p" )
;;
esac
# targetSdk が空のまま出る = マニフェストに宣言がない。UNKNOWN と READ_FAILED は必ず分ける
printf '%s\t%s\t%s\t%s\n' "$( basename " $f ")" "${ pkg :- - }" "${ vc :- - }" "${ tsdk :- UNKNOWN }"
done
期待する出力はこの形です。
artifact package versionCode targetSdk
wallpapers-v210.aab net.example.wallpapers 210 36
ukiyoe-v180.aab net.example.ukiyoe 180 35
older-release.apk net.example.legacy 42 33
broken-artifact.aab - - READ_FAILED
ここで UNKNOWN と READ_FAILED を分けているのには理由があります。両方を空欄にまとめてしまうと、「マニフェストに targetSdkVersion の宣言がないアプリ」と「単に手元の成果物が壊れている・ツールが入っていない」が同じ見た目になります。前者は実際に危険な状態で、後者はこちらの環境の問題です。棚卸しの表で最も避けたいのは、危険なものが環境の問題に紛れることでした。
set -euo pipefail を置きながら、読み取りコマンドだけ || true で受けているのも同じ理由です。1本読めなかっただけで一覧全体が途中で止まると、何本走査できたのかが分からなくなります。止めるべきところと、記録して進むべきところは分けます。
なお targetSdkVersion が宣言されていない場合、Android は minSdkVersion の値を暗黙の target として扱います。マニフェストに書いていないから最新扱いになる、ということはありません。放置期間の長いプロジェクトほど、ここが空白になっていることがあります。
上げる・据え置く・畳むを分ける四つの問い
一覧が出たら、次は行ごとの判断です。私は四つの問いで振り分けています。
そのアプリは今も新規インストールを取っているか。 取っていれば、可視性を失う痛みがそのまま効きます。取っていなければ、可視性のルールは実質的に無害です。
依存の数と、最後にビルドが通ったのはいつか。 上げるコストはコードの量ではなく依存の数で決まります。二年前で止まっているプロジェクトは、targetSdk を書き換える前に依存の更新で止まります。
収益は新規インストールに連動しているか、既存ユーザーに連動しているか。 広告収益は既存ユーザーが使い続ける限り回りますが、買い切りや初回課金は新規流入に直結します。同じ「露出が減る」でも意味が違います。
次の一年で機能追加をする予定があるか。 予定があるなら、据え置きは先送りにしかなりません。
四つの答えを持って表に落とすと、こうなります。
選択 向いているケース 今払うコスト 先送りされるもの 見ておく指標
36 に上げて更新を続ける 新規インストールがあり、次の一年で手を入れる予定がある 依存の更新と実機回帰。依存が多いほど伸びる なし クラッシュ率と ANR。段階公開の各段で確認
35 で据え置く 新規インストールは少ないが、既存ユーザーが残っていて収益が回っている 35 のビルドを一度だけ出す手間 36 対応そのもの。次に更新したくなった日に全額払う 既存ユーザーのクラッシュ率と、依存の脆弱性通知
露出を諦める(何もしない) 新規インストールがほぼゼロで、既存ユーザーにも動きがない なし 復帰の可能性。再開時は要件が一段上がっている 既存ユーザー数の減り方だけ
三行目を用意しているのは、正直な選択肢として必要だからです。手を入れないと決めることと、決めないまま放置することは違います。前者は判断で、後者は判断の欠落です。私自身、この行を表に書けるようになってから、後ろめたさで無駄に触ることが減りました。
直感に反していたのは、「更新しない」が最も安いとは限らないことでした
据え置きを選ぶとき、頭の中では「何もしないのだからコストはゼロ」と計算しがちです。実際には、35 に届いていないアプリを据え置く場合、一度だけビルドを出す必要があります。そしてその一度が、一番高くつくことがあります。
二年触っていないプロジェクトのビルドを通す作業は、多くの場合 targetSdk の書き換えでは終わりません。ビルドツールのバージョン、署名鍵の所在、依存の配布停止。どれか一つでも欠けていると、35 に上げるだけの作業が数日に膨らみます。
実際にぶつかった順で言うと、最初の関門はコードではありませんでした。署名です。アップロード鍵をどこに置いたかが分からなければ、ビルドが通っても提出までは届きません。次が依存の配布停止で、ロックファイルに書かれたバージョンが取得できずに止まります。ここで出るエラーは対象 API レベルとは何の関係もない文言で出るため、原因の見当がつくまでに時間を取られました。
この二つは、事前に確かめておけば回避できる種類の詰まり方です。据え置きを決めた時点で、署名鍵の所在と依存の取得可否だけ先に見ておくと、実際に上げる日の作業が目に見えて短くなります。本番に出すかどうかの判断とは切り離して、ビルドが通るかどうかだけを年に一度確かめる。そういう運用に落ち着きました。
もう一つ、判断を誤りやすい注意点があります。可視性のルールは「新規ユーザーに表示されるかどうか」の話であって、既存ユーザーの更新経路とは別です。据え置きを選ぶ場合は、既に入れている人には何も起きないという前提で構いません。逆に、新規獲得を止めたくないのであれば、露出が落ちてから気づいても遅い、という非対称がここにあります。
だからこそ、判断は「上げるか、据え置くか」ではなく「一度ビルドを通せる状態を保つか、保たないか」に近いと考えるようになりました。要件の水準そのものより、ビルドが通る状態を維持するコストのほうが、長い目では効いてきます。
据え置きを選んだアプリにも、やることは残ります
更新を止めると決めても、監視まで止めると後で困ります。据え置き側で残す作業を最小限に絞ると、四つでした。
ストア掲載情報の整合を保つ。 説明文とスクリーンショットは、バイナリの提出とは別に更新できます。機能していない案内が残っているほうが、露出よりも評価を削ります。
クラッシュ監視のアラートは切らない。 既存ユーザーは残っています。OS 側のアップデートで、こちらが何もしなくても壊れることがあります。
依存の脆弱性通知だけは受け続ける。 対応するかどうかは別の判断ですが、知らないまま置くのとは違います。
最後にビルドが通った環境を記録に残す。 ここが後で最も効きます。
四つ目は具体的に、次の値をリポジトリの中にテキストで残しています。JDK のバージョン、Android Gradle Plugin のバージョン、Gradle のバージョン、Node のバージョン、そしてビルドが通った日付。再開するときに何を復元すればいいかが分かるだけで、初日の消耗がまったく違います。
上げると決めたなら、順序を先に固定します
36 に上げる側は、作業の順序を先に決めておくと手戻りが減ります。私は次の順で進めています。
依存の対応状況を先に洗う。 targetSdk を書き換える前に、依存側が新しい API レベルに追随しているかを確認します。ここを飛ばすと、ローカルのビルドは通るのにストアのプリチェックで止まる、という一番遠回りな失敗に当たります。実効値の並べ方は Rork のプロジェクトで targetSdkVersion 36 を通すまでに直した3か所 に手順を書いています。
compileSdk と targetSdk を設定ファイル側で上げる。 Expo や Rork が生成したプロジェクトでは、android/app/build.gradle を手で書き換えても prebuild の再生成で消えます。設定は app.json の側に寄せます。
実機回帰は最小構成で行う。 起動、権限ダイアログ、フォアグラウンドサービス、バックグラウンドからの復帰。この四つだけ先に見ます。
段階公開で出す。 一度に全ユーザーへ配らない、というだけでリスクの形が変わります。
2 の設定はこの形です。
{
"expo" : {
"plugins" : [
[
"expo-build-properties" ,
{
"android" : {
"compileSdkVersion" : 36 ,
"targetSdkVersion" : 36 ,
"minSdkVersion" : 24
}
}
]
]
}
}
minSdkVersion をここに明示しているのは、上げる作業のついでに下限を動かしてしまう事故を防ぐためです。下限の変更は、対象 API レベルの要件とはまったく別の判断です。同じコミットで一緒に動かすと、後から「なぜこの端末が切れたのか」を追えなくなります。
native ディレクトリに手で入れた変更がどれだけ残っているかを、上げる前に数えておく方法は prebuild --clean で消える手入れを、上げる前に数える にまとめています。依存側で何を見るかは targetSdk を 36 にした後、依存ライブラリのどこを見るか が近い話です。
期限を過ぎてから、何がどの順で起きるか
期限当日を過ぎると、変化はまとめてではなく順番に来ます。
まず、要件を満たさないビルドの提出が通らなくなります。ここは即時です。次に、すでに公開されているアプリの掲載はそのまま続きます。ストアから消えるわけではありません。そして可視性のルールが効いているアプリでは、新しい Android を積んだ端末の新規ユーザーに対して、検索や一覧に出なくなります。既にインストールしている人には影響がありません。
最後に来るのが、更新を再開したくなった日です。据え置きで浮かせたコストは消えていません。再開の日にまとめて払うことになり、しかもその時点では要件が一段上がっています。
この順序を先に理解しておくと、期限当日に焦って全部を上げようとせずに済みます。今年のように延長申請の窓がある場合も、締切そのものより「間に合わせるか、申請に切り替えるか」を決める日のほうが先に来ます。判断を切り替える締切は、作業の締切より前にあります。
来年の同じ日に、同じ調べ物をしないために
年次の要件は毎年来ます。だから棚卸しの結果は、一覧を眺めて終わりにせず、判断ごと残します。
先ほどのスクリプトの出力に、判断列を足した台帳を作ります。
# 棚卸し台帳: 判断と理由を手で埋める前提の骨組みを作る
./target-sdk-inventory.sh ~/builds \
| awk -F '\t' '
NR==1 { print "artifact,package,versionCode,targetSdk,last_release,decision,reason"; next }
{ print $1 "," $2 "," $3 "," $4 ",,," }
' > inventory-2026-08.csv
decision には raise / hold / retire のいずれかを、reason には一行で理由を書きます。翌年は同じスクリプトを走らせて、去年の台帳と突き合わせるだけで済みます。去年 hold と書いたアプリが今年も hold でいいのか、という問いから始められるのは、白紙から調べ直すのとはまったく別の速さです。
理由の欄を空にしないことが肝心でした。数値だけ残しても、来年の自分はなぜそう決めたのかを思い出せません。「新規ゼロ・既存 800 人・広告のみ」のような短い一行があるだけで、判断は再利用できるようになります。
今日のうちに決めておくこと
やることを一つに絞るなら、手元の成果物を一度並べて、据え置くつもりのアプリの targetSdk が 35 に届いているかだけを確認してください。36 の話はその後で構いません。届いていないアプリが一本でもあれば、そこが今年いちばん静かに損をしている場所です。
私自身、据え置きを選んだアプリの一覧の前で何度も手が止まりました。同じところで迷っている方に、答えではなく決め方のほうを渡せていれば嬉しいです。