Google Play Console で targetSdkVersion を 34 から 35 に上げた直後、Pixel 7 の実機で確認したら下部タブバーがジェスチャーナビゲーションのインジケータの裏に潜り込んでいました。プレビューでは何も問題なかった画面が、Android 15 を載せた端末だけで崩れる — このパターンに最近よく出会います。
これは Android 15 から targetSdkVersion 35 以上のアプリに対してエッジ・トゥ・エッジ表示が強制されるようになったことが原因です。Rork で生成された初期コードは React Native の SafeAreaView を画面上部にだけ使っているケースが多く、Android 15 のシステムバー領域の扱いが変わったことに引きずられてレイアウトが下まで伸びてしまいます。
2014年からアプリを個人で出してきた中で、OS のメジャーアップデートに伴うレイアウト崩れには何度も振り回されてきました。今回の Android 15 はその中でもインパクトが大きい部類で、累計5,000万ダウンロードを支えてきた既存アプリ群のいくつかも、targetSdkVersion 引き上げのタイミングで一度作り直しました。ここでは私が実際にハマって整理した対処パターンを、Rork で生成したアプリにそのまま適用できる形でまとめます。
まず確認すべきこと — targetSdkVersion と Android バージョン
問題が起きているのが本当にエッジ・トゥ・エッジ強制が原因なのか、最初に切り分けます。
# app.json または app.config.ts で確認
"android": {
"targetSdkVersion": 35,
"compileSdkVersion": 35
}targetSdkVersion が 34 以下なら、エッジ・トゥ・エッジは強制されません。Android 15 の端末でも従来通り「システムバーを避ける」挙動になります。
一方で、Google Play は 2026年8月以降に提出するアプリに targetSdkVersion: 35 を必須化したため、ストア更新を予定しているなら避けては通れません。Rork が生成する Expo プロジェクトでは、expo prebuild 後の android/app/build.gradle で targetSdkVersion が決まります。
実機での再現は次のコマンドで Android のバージョンを確認するのが早道です。
adb shell getprop ro.build.version.release
# 15
adb shell getprop ro.build.version.sdk
# 35これで「Android 15 端末で targetSdkVersion: 35 のビルド」だと確定したら、以下の対処に進みます。
対処 1: react-native-edge-to-edge を導入する
Expo SDK 53 以降では react-native-edge-to-edge が Expo の推奨対応として組み込まれました。Rork が出すコードがこれを使っていない場合は、まずインストールから入ります。
npx expo install react-native-edge-to-edgeapp.json の android セクションに次を追記します。
{
"expo": {
"android": {
"edgeToEdgeEnabled": true
}
}
}これだけでは画面下端のレイアウトは直りません。エッジ・トゥ・エッジは「システム UI と重なる前提でアプリ側がインセットを処理する」という思想なので、コンポーネント側の対応が必要です。
対処 2: useSafeAreaInsets で下部インセットを取る
SafeAreaView だけで囲っている画面では、Android 15 の動作モデルの変更で下部インセットが思った通りに取れません。useSafeAreaInsets で実数値を取り、paddingBottom に渡す方法が確実です。
// app/(tabs)/_layout.tsx
import { Tabs } from "expo-router";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import { Platform } from "react-native";
export default function TabLayout() {
const insets = useSafeAreaInsets();
return (
<Tabs
screenOptions={{
tabBarStyle: {
// Android 15 のジェスチャーナビゲーション領域を確保
paddingBottom: Platform.OS === "android" ? insets.bottom : 0,
height: 56 + (Platform.OS === "android" ? insets.bottom : 0),
},
}}
>
<Tabs.Screen name="index" options={{ title: "Home" }} />
<Tabs.Screen name="settings" options={{ title: "Settings" }} />
</Tabs>
);
}ジェスチャーナビゲーションを使っている端末は insets.bottom が 0 に近い値(多くは 16dp 程度)になり、3 ボタンナビゲーションでは 48dp 前後になります。固定値ではなく、必ず insets から動的に取ってください。
対処 3: 画面コンテンツのスクロール領域に下端余白を持たせる
タブバーを直したあとに次にぶつかるのが、ScrollView や FlatList の最終アイテムがタブバーの裏に隠れる問題です。contentContainerStyle で paddingBottom を確保します。
import { ScrollView } from "react-native";
import { useSafeAreaInsets } from "react-native-safe-area-context";
export default function HomeScreen() {
const insets = useSafeAreaInsets();
const TAB_BAR_HEIGHT = 56;
return (
<ScrollView
contentContainerStyle={{
paddingBottom: TAB_BAR_HEIGHT + insets.bottom + 16,
}}
>
{/* リストの中身 */}
</ScrollView>
);
}ここで TAB_BAR_HEIGHT を定数として外に切り出しておくと、後から高さを変えた時に複数画面の同期が楽になります。私は constants/layout.ts のような場所に集めるようにしています。
対処 4: ステータスバー領域の重なりを直す
下部だけでなく、上部のヘッダーがステータスバーに食い込むケースもあります。Rork が expo-status-bar を使って translucent を true にしている時に多発します。
// app/_layout.tsx
import { StatusBar } from "expo-status-bar";
import { SafeAreaProvider, SafeAreaView } from "react-native-safe-area-context";
import { Stack } from "expo-router";
export default function RootLayout() {
return (
<SafeAreaProvider>
<SafeAreaView style={{ flex: 1 }} edges={["top"]}>
<StatusBar style="auto" />
<Stack />
</SafeAreaView>
</SafeAreaProvider>
);
}SafeAreaProvider を最上位に、SafeAreaView の edges を ["top"] に絞るのがポイントです。edges を指定しないと iOS の下部インジケータ領域までインセットを取りに行ってしまい、Android との挙動差が大きくなります。
対処 5: モーダルとボトムシートの底面処理
react-native-modal や @gorhom/bottom-sheet を使ったボトムシートも、Android 15 では下端の挙動が変わります。シートの中身に下部インセットを足し忘れると、CTA ボタンがジェスチャー領域と被って誤タップを誘発します。
import BottomSheet, { BottomSheetView } from "@gorhom/bottom-sheet";
import { useSafeAreaInsets } from "react-native-safe-area-context";
export function CommentSheet() {
const insets = useSafeAreaInsets();
return (
<BottomSheet snapPoints={["50%", "90%"]}>
<BottomSheetView style={{ paddingBottom: insets.bottom + 24, flex: 1 }}>
{/* シートのコンテンツ */}
</BottomSheetView>
</BottomSheet>
);
}ボトムシート系は内部でステータスバーやキーボードの処理を持っているライブラリが多いため、ライブラリの README に「edge-to-edge 対応」「Android 15 対応」の項目があるかも合わせて確認してください。
動作確認のチェックリスト
実機で確認する際は、以下の3パターンを最低限カバーしておくと安心です。
- ジェスチャーナビゲーション有効の Pixel 系(
insets.bottomが小さい) - 3 ボタンナビゲーション固定の Samsung 端末(
insets.bottomが大きい) - Android 14 以下の端末(targetSdkVersion 35 でもエッジ・トゥ・エッジは強制されない)
Pixel エミュレータでも再現できますが、Samsung One UI のような OEM カスタマイズではインセットが微妙に違うため、可能なら実機を一台確保しておくと心強いです。私は中古の Galaxy A シリーズを 1 台、検証専用に置いています。
予防策 — レイアウトの最初の一行から考える
Android 15 のエッジ・トゥ・エッジ強制は、新規プロジェクトでは最初から前提として組み込めば困りません。新しい画面を作るたびに「この画面のインセットは誰が処理するか」を意識して書いておくと、後で OS が変わってもまとめて直す手間がほぼゼロになります。
私の場合は、画面ごとに SafeAreaView または useSafeAreaInsets のどちらを使うかを最初に決めて、画面ファイルの先頭にコメントで残すルールにしています。チーム開発でなくても、半年後の自分への申し送りとして効果があります。
OS のアップデートは止められませんが、レイアウトの考え方を整えておけば、変更点ごとに振り回されることは減らせます。同じ問題に取り組んでいる方の参考になれば幸いです。