RORK LABEN
DEADLINE — Google Play の Android 16(API レベル36)必須化は明日8月31日です。残り1日で、新規アプリも既存アプリの更新も対象になりますDECISION — 延長申請は期限を過ぎてからでは提出できません。間に合わせるのか申請へ切り替えるのか、その判断そのものの締切が今日という形になりますVISIBILITY — 更新を止めているアプリも無関係ではありません。API レベル35 未満のままだと、新しい Android 端末の新規ユーザーには表示されなくなりますPITFALL — 依存ライブラリ側が新しい API レベルに追随していない場合、ローカルのビルドは通ってもストアのプリチェックで止まることがあります。自分のコードだけを見ていても分かりませんEXPO — expo@57.0.17 が React Native を 0.86.3 へ更新しました。大きな SDK リリースの合間に破壊的変更のないパッチを挟む流れが見えてきており、追随のコストが下がる方向ですCHOICE — Android も出すなら React Native を生成する本家 Rork、Apple プラットフォーム固有の機能が要件の中心なら Swift を生成する Rork Max。優劣ではなく何を出すかで決まりますDEADLINE — Google Play の Android 16(API レベル36)必須化は明日8月31日です。残り1日で、新規アプリも既存アプリの更新も対象になりますDECISION — 延長申請は期限を過ぎてからでは提出できません。間に合わせるのか申請へ切り替えるのか、その判断そのものの締切が今日という形になりますVISIBILITY — 更新を止めているアプリも無関係ではありません。API レベル35 未満のままだと、新しい Android 端末の新規ユーザーには表示されなくなりますPITFALL — 依存ライブラリ側が新しい API レベルに追随していない場合、ローカルのビルドは通ってもストアのプリチェックで止まることがあります。自分のコードだけを見ていても分かりませんEXPO — expo@57.0.17 が React Native を 0.86.3 へ更新しました。大きな SDK リリースの合間に破壊的変更のないパッチを挟む流れが見えてきており、追随のコストが下がる方向ですCHOICE — Android も出すなら React Native を生成する本家 Rork、Apple プラットフォーム固有の機能が要件の中心なら Swift を生成する Rork Max。優劣ではなく何を出すかで決まります
記事一覧/Rork入門
Rork入門/2026-08-30中級

expo-doctor は API レベル 36 を見ていません。22 項目の中身を読んで分かったこと

expo-doctor 1.20.4 の配布物を開き、登録されている22チェックを一覧にしました。ストア提出を見る項目は1つだけで、基準は API レベル34のままでした。

expo-doctorExpo SDK 575Google Play31targetSdkVersion3Rork547

昨日の夕方、公開中の Android アプリを並べて targetSdkVersion を数え直していました。Google Play の API レベル 36 必須化が明日に迫っているためです。個人開発で複数本を抱えていると、どれが片づいていてどれが残っているのか、記憶はあてになりません。

一通り確認したあと、念のため npx expo-doctor を通しました。全項目が緑で返ってきました。

そこで安心しかけて、手が止まりました。この緑は、何を保証してくれた緑なのでしょうか。

恥ずかしながら、私は expo-doctor が何を見ているのかを正確には把握していませんでした。配布物を開けば読める話です。読んだ結果、期限に関わる部分については、想像とかなり違っていました。

expo-doctor 1.20.4 に登録されている 22 項目

インストールせずに中身だけ確認したかったので、npm レジストリから配布物を直接取り出しました。プロジェクトを汚さずに済みます。

META=$(curl -s https://registry.npmjs.org/expo-doctor)
 
echo "$META" | node -e "
let s=''; process.stdin.on('data',d=>s+=d).on('end',()=>{
  const j=JSON.parse(s); const v=j['dist-tags'].latest;
  console.log('latest =', v);
  console.log('tarball =', j.versions[v].dist.tarball);
});"
# latest  = 1.20.4
# tarball = https://registry.npmjs.org/expo-doctor/-/expo-doctor-1.20.4.tgz
 
curl -sL https://registry.npmjs.org/expo-doctor/-/expo-doctor-1.20.4.tgz -o expo-doctor.tgz
tar xzf expo-doctor.tgz

package/build/index.js は 984 KB の単一バンドルで、各チェックはクラスとして並んでいます。クラス名・説明文・適用される SDK バージョン範囲は、次の 1 コマンドで取り出せます。

node -e "
const s = require('fs').readFileSync('package/build/index.js', 'utf8');
const re = /class\s+(\w+Check\w*)\s*\{description=\"([^\"]+)\";sdkVersionRange=\"([^\"]*)\"/g;
let m;
while ((m = re.exec(s))) console.log(m[1].padEnd(46), m[3].padEnd(20), m[2]);
"

出力された 22 項目を、適用範囲とあわせて整理したものが次の表です。範囲が * の項目は SDK バージョンに関係なく走ります。

チェック名適用 SDK 範囲見ている内容
StoreCompatibilityCheck*ストア提出のバージョン要件
ExpoConfigSchemaCheck*app.json / app.config.js のスキーマ
ExpoConfigCommonIssueCheck*Expo 設定にありがちな記述ミス
AppConfigFieldsNotSyncedToNativeProjectsCheck*非 CNG 構成で native 側へ反映されない設定項目
PackageJsonCheck*package.json の一般的な問題
PackageManagerVersionCheck*npm / yarn のバージョン
LockfileCheck*ロックファイルの有無
EnvLocalFilesCheck*ローカル環境ファイルのコミット混入
NativeToolingVersionCheck*native ツールチェーンのバージョン
PeerDependencyChecks*必須 peer 依存の不足
ProjectSetupCheck*プロジェクト構成のよくある不備
IllegalPackageCheck>=44.0.0native モジュールが持つ非互換なサポートパッケージ
SupportPackageVersionCheck>=45.0.0 <54.0.0サポートパッケージのバージョン整合
GlobalPackageInstalledLocallyCheck>=46.0.0旧グローバル CLI のローカル混入
InstalledDependencyVersionCheck>=46.0.0SDK が要求するバージョンとの一致
MetroConfigCheck>=51.0.0Metro 設定の問題
ReactNativeDirectoryCheck>=51.0.0React Native Directory のメタデータ照合
AutolinkingDependencyDuplicatesCheck>=54.0.0依存の重複インストール
DependencyVersionOverrideCheck>=55.0.0上書きされた依存バージョン
HermesV1VersionCheck>=55.0.0 <58.0.0Hermes V1 の退行を含む SDK バージョン
VectorIconsCheck>=56.0.0@expo/vector-icons と競合するアイコンパッケージ
ExpoRouterReactNavigationCheck>=56.0.0 <57.0.0expo-router と @react-navigation の同居

眺めていて気づくのは、大半が「依存関係とプロジェクト構成の健康診断」であることです。パッケージのバージョン、設定ファイルの書式、重複、ロックファイル。手元の作業環境が壊れていないかを見る道具として、非常によくできています。

そして、ストア側の要件に触れている項目は、この 22 のうち 1 つだけです。

ストアを見ている項目は 1 つで、基準は 2024 年のまま

StoreCompatibilityCheck の定義部分を取り出すと、判定に使う定数がそのまま書かれています。

// package/build/index.js から抜粋(expo-doctor 1.20.4)
u.PLAY_STORE_MINIMUM_REQS = {
  effectiveDate: "August 31 2024",
  AndroidSdkVersion: 34,
  ExpoSdkVersion: 50
};

effectiveDate が 2024 年 8 月 31 日、基準となる API レベルが 34 です。これは 2 年前の Google Play の要件であり、明日 2026 年 8 月 31 日から適用される API レベル 36 の要件ではありません。

判定のロジックも素直で、targetSdkVersion が 34 未満のときだけ警告を出します。34、35、36 はいずれも「問題なし」として扱われます。つまり、targetSdkVersion 35 のまま放置しているアプリに対して、expo-doctor は今日も緑を返します。

android ディレクトリを持たない CNG 構成の場合は、expo-build-properties プラグインの android.targetSdkVersion を見て、それもなければ Expo SDK が 50 以上かどうかで判定します。SDK 57 を使っていれば、この分岐でも当然のように通過します。

名前は StoreCompatibilityCheck と複数形ですが、参照している定数は PLAY_STORE_MINIMUM_REQS の 1 つだけです。バンドル内に App Store 側の要件を表す定数は見当たりませんでした。iOS の最低 OS バージョンや Xcode の要件は、この 22 項目の外にあります。App Store と Google Play の両方へ出している場合、片方しか見ていない道具だという前提で読む必要があります。

これは expo-doctor の欠陥というより、道具の役割の問題だと私は受け止めています。expo-doctor はプロジェクトの整合性を見る道具であって、ストアの審査要件を追いかける道具として設計されてはいません。ただ、名前と出力の見た目から「提出前の最終確認」として使ってしまう人は、私を含めて少なくないはずです。

build.gradle に書いた値が読まれない条件

もう一段掘ってみると、android ディレクトリがある構成では、さらに手前で判定が止まる場合がありました。値の読み取りに使われている正規表現は次のものです。

const E = /^\s*targetSdkVersion\s*=\s*(?:Integer\.parseInt\(findProperty\('android\.targetSdkVersion'\)\s*\?:\s*'(\d+)'\)|'(\d+)')/m;
const w = i.match(E)?.[1];   // 参照しているのは 1 番目のキャプチャグループ

括弧が 2 つあります。1 番目は Integer.parseInt(findProperty(...) ?: '35') 形式の中身、2 番目は targetSdkVersion = '35' のような素のクォート形式です。ところが値を取り出す側は 1 番目しか参照していません。素のクォート形式で書かれていると、正規表現自体は一致するのに、読み取られる値は undefined になります。

同じ定数と正規表現をそのまま持ってきて、書き方を変えながら通してみました。

// check-store-gate.mjs
const PLAY_STORE_MINIMUM_REQS = { AndroidSdkVersion: 34 };
const TARGET_SDK_RE =
  /^\s*targetSdkVersion\s*=\s*(?:Integer\.parseInt\(findProperty\('android\.targetSdkVersion'\)\s*\?:\s*'(\d+)'\)|'(\d+)')/m;
 
function judge(gradleText) {
  const raw = gradleText.match(TARGET_SDK_RE)?.[1];
  const value = raw ? parseInt(raw, 10) : undefined;
  const flagged = !!(value && value < PLAY_STORE_MINIMUM_REQS.AndroidSdkVersion);
  return { matched: TARGET_SDK_RE.test(gradleText), value, verdict: flagged ? '警告あり' : '警告なし' };
}
 
const samples = [
  ["targetSdkVersion = '33'", "ext {\n  targetSdkVersion = '33'\n}"],
  ["targetSdkVersion = '35'", "ext {\n  targetSdkVersion = '35'\n}"],
  ['targetSdkVersion = 35',   'ext {\n  targetSdkVersion = 35\n}'],
  ["parseInt(... ?: '33')",   "ext {\n  targetSdkVersion = Integer.parseInt(findProperty('android.targetSdkVersion') ?: '33')\n}"],
  ["parseInt(... ?: '35')",   "ext {\n  targetSdkVersion = Integer.parseInt(findProperty('android.targetSdkVersion') ?: '35')\n}"],
  ['(ext ブロックなし)',        'apply plugin: "expo-root-project"'],
];
 
for (const [label, text] of samples) {
  const r = judge(text);
  console.log(label.padEnd(26), 'regex一致=' + String(r.matched).padEnd(6),
              '読み取り値=' + String(r.value).padEnd(10), r.verdict);
}

手元の Node.js 22.23.2 で実行した結果です。

build.gradle の書き方正規表現の一致読み取られた値判定
targetSdkVersion = '33'一致undefined警告なし
targetSdkVersion = '35'一致undefined警告なし
targetSdkVersion = 35不一致undefined警告なし
Integer.parseInt(... ?: '33')一致33警告あり
Integer.parseInt(... ?: '35')一致35警告なし
ext ブロックなし不一致undefined警告なし

targetSdkVersion = '33' と書いてあっても警告は出ません。値を読み取れているのは Integer.parseInt(findProperty(...)) 形式だけでした。

さらに、SDK 57 の bare テンプレート(expo-template-bare-minimum@57.0.20)の android/build.gradle を実際に取り出して確認したところ、ext ブロックに targetSdkVersion の記述そのものがありませんでした。android/app/build.gradle の 94 行目が targetSdkVersion rootProject.ext.targetSdkVersion を参照し、その値は expo-root-project プラグイン側で決まる構成になっています。この形では、そもそも正規表現が当たる文字列が存在しません。

つまり現行 SDK の標準構成では、StoreCompatibilityCheck は android ディレクトリを見つけても、値を読めないまま通過します。緑が返ってくるのは当然でした。

黙らせられる項目と、黙らせない方がよい項目

バンドルを読んでいて、もう一つ実務で効きそうな部分が見つかりました。いくつかのチェックは環境変数と package.json の設定で挙動を変えられます。

指定方法効果私の使い分け
EXPO_DOCTOR_SKIP_DEPENDENCY_VERSION_CHECKSDK 要求バージョンとの照合チェックを登録しない常用しない。意図的に固定している依存がある短期間のみ
EXPO_DOCTOR_ENABLE_DIRECTORY_CHECKReact Native Directory 照合の有効・無効を強制。設定より優先CI で外部 API に依存させたくないときに無効化
EXPO_DOCTOR_WARN_ON_NETWORK_ERRORS失敗が全て通信エラー起因なら終了コードを落とさないCI では入れておく。ネットワークの揺れでビルドを止めない
expo.doctor.reactNativeDirectoryCheck.exclude照合から除外するパッケージ。/正規表現/ 形式も可社内・自作モジュールの登録に使う
expo.doctor.reactNativeDirectoryCheck.listUnknownPackagesfalse でメタデータ未登録パッケージの警告を抑制ノイズが多いときだけ
expo.doctor.appConfigFieldsNotSyncedCheck.enabled非 CNG 構成向けチェックの有効・無効触らない

EXPO_DOCTOR_WARN_ON_NETWORK_ERRORS の挙動は、実装を読むと分かりやすい形でした。失敗したチェックのうち通信エラー起因のものを数え、それが失敗全体と同数のときだけ、終了コードを落とさずに戻ります。通信エラー以外の失敗が 1 件でも混ざっていれば、通常どおり失敗として扱われます。CI に入れておいて実害が出にくいのは、この設計のおかげです。

一方で EXPO_DOCTOR_SKIP_DEPENDENCY_VERSION_CHECK は、私は常用しないようにしています。これを付けるとチェック自体が登録されず、代わりに「無効化されています」というログが 1 行出るだけになります。無効にした事実は残りますが、次にそのリポジトリを触る自分がログを読むとは限りません。

明日以降、何で確認するか

今回分かったことを、自分向けの結論として 3 行にまとめておきます。

expo-doctor の緑は、依存関係とプロジェクト構成が整っている証明です。ストアの現行要件を満たしている証明ではありません。API レベル 36 の判定には、別の手段が要ります。

実効値の確認は、ビルド成果物か Play Console 側で行うのが確実です。手元の設定ファイルの見た目と、実際にビルドへ渡っている値がずれる箇所については、Rork のプロジェクトで targetSdkVersion 36 を通すまでに直した3か所に整理しています。依存ライブラリ側が引きずってくる要素については、targetSdk を 36 にした後、依存ライブラリのどこを見るかが近い話題です。

そして、今日の作業としてお勧めしたいことが 1 つあります。手元のアプリの android/app/build.gradle を開き、targetSdkVersion が直値なのか rootProject.ext 経由なのかを確認してください。後者であれば、expo-doctor の判定はその値に届いていません。数分で終わります。自分が使っている Expo のパッチ版を把握しておく手順は、30日で9回出た Expo のパッチのうち、自分のアプリに入っているのはどれかを確かめるにまとめました。

道具を疑ったというより、道具の担当範囲を勘違いしていたのは私のほうでした。読んでよかったと思っています。お読みいただきありがとうございました。

シェア

お読みいただきありがとうございます

Rork Lab は広告なしで運営しており、サーバー費用などの運営コストはメンバーシップのご支援で賄っています。実装コード・ベンチマーク・本番設計パターンなど、実務でお役立ていただける記事を毎日更新しています。もし読んでよかったと感じていただけましたら、ぜひご覧ください。

  • コピー&ペーストで使える実装コード付き
  • 毎日新しい上級ガイドを追加
  • ¥580/月 または ¥2,480 の永久アクセス
メンバーシップを見る →

もしこの記事がお役に立ちましたら、チップ(¥150)で応援いただけると大変励みになります。広告なしでの運営を続けるため、皆さまのご支援が大きな力になっています。

関連記事

Rork 入門2026-08-22
変えられる名前と、変えられない名前 — Rork で最初の1本を公開する前の30分
アプリには名前が3種類あり、公開後に変えられるのは表示名だけです。Rork が既定で入れる ID をそのまま提出する前に、依存ゼロのスクリプトで検査する手順をまとめました。
Rork 入門2026-08-19
審査がいつ明けるか読めないなら、公開日は審査の外側で決めます
審査の所要時間は開発者側で短縮できませんが、公開の瞬間は自分で決められます。App Store Connect の3つのリリース設定と Google Play の公開の管理、そして eas submit との噛み合わせを整理します。
Rork 入門2026-04-18
RorkアプリをiOS・Android同時公開したときの手順と、プラットフォームごとに詰まったこと
RorkアプリをApp StoreとGoogle Playに同時公開した実体験をもとに、スクリーンショット仕様・プライバシー設定・審査期間の違いなど、両プラットフォームで詰まりやすいポイントを具体的に解説します。
📚RECOMMENDED BOOKS
大規模言語モデル入門
山田育矢
LLM開発
生成AIプロンプトエンジニアリング入門
我妻幸長
プロンプト
Claude CodeによるAI駆動開発入門
平川知秀
AI駆動開発
※ アフィリエイトリンクを含みます
もっと見る →