◉RORK LABEN
●GPT-6.1 SOL — Rork のモデルメニューに追加(09-29)。Low/Medium/High を選べ、Pro・Max に含まれます●SONNET 5.5 — UI づくりが得意で、Sonnet 5 より出力が速いモデルが選べます(09-28)●SDK 58 — Beta が 09/15 から。stable の案内はまだなく、Expo Go でのプロジェクト実行は 09/03 からログインが必要です●WIDGET — iOS のウィジェットが本番ビルドだけ真っ黒になる原因を追った報告が出ています●OBSERVE — EAS Observe がネイティブクラッシュも記録します(SDK 57.0.21 以降・10/07)●NEW — 起動広告が、背面から戻った回にしか出ていませんでした。「前面に戻った」合図が、…●GPT-6.1 SOL — Rork のモデルメニューに追加(09-29)。Low/Medium/High を選べ、Pro・Max に含まれます●SONNET 5.5 — UI づくりが得意で、Sonnet 5 より出力が速いモデルが選べます(09-28)●SDK 58 — Beta が 09/15 から。stable の案内はまだなく、Expo Go でのプロジェクト実行は 09/03 からログインが必要です●WIDGET — iOS のウィジェットが本番ビルドだけ真っ黒になる原因を追った報告が出ています●OBSERVE — EAS Observe がネイティブクラッシュも記録します(SDK 57.0.21 以降・10/07)●NEW — 起動広告が、背面から戻った回にしか出ていませんでした。「前面に戻った」合図が、…
記事一覧/開発ツール
⬡ 開発ツール/2026-04-27中級

RorkのAndroidアプリで戻るボタンが効かない — BackHandlerの落とし穴と直し方

Rorkで作ったAndroidアプリで、戻るボタンを押すたびにアプリが終了してしまう・想定外の画面に戻る・モーダルが閉じない。BackHandlerが効かない典型的な原因と、Expo Routerでの正しい実装パターンを実コード付きで解説します。

Rork585Android55BackHandlerExpo Router6トラブルシューティング85

Rork で作った Android アプリを Internal Testing で配布したところ、テスターから「戻るボタンを押したらアプリが落ちる(終了する)」というフィードバックが立て続けに届く——これは Rork で iOS 中心に開発している方が、Android で初めて出くわす盲点のひとつです。

iOS にはハードウェアの戻るボタンが存在しないため、テストでは気づきません。Android ではほとんどのユーザーが画面下のジェスチャーまたはハードウェアボタンで「戻る」を多用します。ここが正しく動かないアプリは、レビューで容赦なく星1を付けられます。

ここではRork が生成した React Native + Expo Router 構成のアプリで、Android の戻るボタンが効かない・想定外に動く5つのパターンと、それぞれの直し方を実コード付きで整理します。

まず確認したい3つのこと

トラブルシューティングに入る前に、以下を切り分けます。

  • アプリ全体で効かないのか、特定の画面だけか
  • モーダル表示中だけ効かないのか
  • Development Build と Production Build で挙動が違うか

特に「特定の画面だけ効かない」ケースは、Expo Router のスタック構成と BackHandler の登録タイミングがずれているサインです。Expo Router 全体の挙動については Rork × Expo Routerで起きる画面遷移エラーの原因と解決策 も合わせて読むと理解が深まります。

原因1: useEffectで登録してクリーンアップを忘れている

最もよく見かけるのがこのパターンです。

// ❌ 効かないコード
import { BackHandler } from 'react-native';
 
export default function ChatScreen() {
  useEffect(() => {
    BackHandler.addEventListener('hardwareBackPress', () => {
      // 確認ダイアログを出したい
      return true;
    });
    // ← removeEventListener を呼んでいない
  }, []);
}

このコードは初回は動いているように見えますが、画面を離れて戻ってきたあと、リスナーが多重登録されて挙動が壊れます。最終的にアプリ全体でハードウェア戻るが無効化されることもあります。

// ✅ 正しいコード
useEffect(() => {
  const sub = BackHandler.addEventListener('hardwareBackPress', () => {
    return true; // true を返すと「戻る」を消費し、デフォルト挙動を止める
  });
  return () => sub.remove();
}, []);

返り値の sub.remove() を必ず cleanup で呼ぶこと。これが BackHandler を扱う上での最重要ルールです。

原因2: useFocusEffectを使っていない

スタックナビゲーション内の特定画面でだけ戻るボタンの挙動を変えたい場合、useEffect ではなく useFocusEffect を使います。

import { useFocusEffect } from 'expo-router';
import { useCallback } from 'react';
 
useFocusEffect(
  useCallback(() => {
    const sub = BackHandler.addEventListener('hardwareBackPress', () => {
      // この画面がアクティブな間だけ実行される
      Alert.alert('保存していない変更があります', '本当に戻りますか?', [
        { text: 'キャンセル', style: 'cancel' },
        { text: '戻る', onPress: () => router.back() },
      ]);
      return true;
    });
    return () => sub.remove();
  }, [])
);

useEffect で登録すると、画面がアンマウントされるまでリスナーが残り続けるため、別の画面に遷移しても確認ダイアログが出るようになってしまいます。useFocusEffect はフォーカスを失った瞬間に cleanup を走らせるので、画面ごとに独立した戻る挙動を組めます。

原因3: trueを返していない(または条件分岐の漏れ)

BackHandler のリスナーは true を返すとデフォルト挙動(前画面に戻る、または最上位ならアプリ終了)を抑止します。false または undefined を返すとデフォルト挙動が走ります。

// ❌ うっかりミス
BackHandler.addEventListener('hardwareBackPress', () => {
  if (modalVisible) {
    setModalVisible(false);
    // ← return true を書き忘れると、モーダルを閉じつつアプリも終了する
  }
  return false;
});
 
// ✅ 修正版
BackHandler.addEventListener('hardwareBackPress', () => {
  if (modalVisible) {
    setModalVisible(false);
    return true; // ここが命
  }
  return false; // モーダルが開いていないときはデフォルト挙動に任せる
});

「モーダルを閉じてアプリも終了する」という意味不明な挙動の原因はこれです。条件分岐ごとに return を明示する癖をつけてください。

原因4: モーダルやBottomSheetがハードウェア戻るを横取りしている

react-native-modal や @gorhom/bottom-sheet などのライブラリは、内部で独自の BackHandler リスナーを登録しています。優先順位は「あとから登録したリスナーが先に呼ばれる」仕様のため、画面側のリスナーが先に登録されていると、モーダルを閉じる動作が動かなかったり、逆にモーダルが閉じる前に画面遷移してしまったりします。

対策は以下の2つです。

  • モーダルを開くタイミングで画面側のリスナーを一時的に外す
  • モーダル内で完結する戻る処理は、ライブラリ標準の onBackdropPress や enableHandlePanningGesture に任せる

複雑なモーダル制御で詰まった場合は Rorkでモーダルが閉じない・タップが効かない問題を直す手順 も参考にしてください。

原因5: Expo Routerのスタック構成が想定と違う

Expo Router の (tabs) グループや Stack.Screen の入れ子構成によっては、戻るボタンが期待とは別のスタックを巻き戻すことがあります。

確認のコツは、画面コンポーネント内で router.canGoBack() の戻り値をデバッグログに出すことです。

import { router } from 'expo-router';
 
useEffect(() => {
  console.log('canGoBack:', router.canGoBack());
}, []);

canGoBack() が false を返している画面でハードウェア戻るを押すと、デフォルトでアプリが終了します。これは仕様通りの動きで、Modal 表示中以外でこの画面に到達してしまう設計を見直す必要があります。(tabs) のルート画面でモーダルを開いている場合などによく起きます。

iOS と Android で挙動が分かれる現象全般については RorkでiOSとAndroidで挙動が違うときの切り分けと修正 に体系的にまとめてあります。

修正前後を実機で確認する

修正したら、必ず以下の順で実機検証します。エミュレータでは再現しないバグも多いので、最低でも Android 12 以上の実機で確認してください。

  1. ハードウェア戻るボタンで意図した画面に戻るか
  2. ジェスチャーナビゲーション(画面端スワイプ)で同じ挙動になるか
  3. モーダルを開いた状態で戻るを押したとき、モーダルだけが閉じるか
  4. ダブルタップで「もう一度押すと終了」のパターンを実装しているなら、トーストが正しく出るか

3 と 4 は Production Build でしか正しく挙動しないことがあるので、eas build --profile production でビルドしてから確認してください。

戻るボタンの設計はAndroid体験の土台

Android の「戻る」は、iOS の左上の < ボタンよりもはるかに頻繁に押されるインタラクションです。ここが期待通りに動かないアプリは、機能の素晴らしさに関係なく、ユーザーから「使いづらい」と即断されます。

今日まずできる一歩は、自分のアプリで BackHandler を使っている画面をすべて洗い出し、useFocusEffect + sub.remove() + 明示的な return true/false の3点セットで書き直すことです。これだけで体感の安定感が大きく変わります。

シェア

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

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

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

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

関連記事

⬡ 開発ツール2026-10-07
日本語の見出しだけ太字にならない — Android で fontWeight が効かないときの切り分けと直し方
Rork で作ったアプリで、iPhone では太字なのに Android では日本語の見出しが細いままになる症状の切り分け記事です。原因の見分け方、fontFamily を重みごとに分ける直し方、確認用コードを載せています。
⬡ 開発ツール2026-10-15
EXPO_PUBLIC_ の値が、開発では読めて本番だけ undefined になる書き方を、変換結果で見分けます
API の URL が開発中は読めるのに、ビルドした途端に undefined になる。原因は process.env の書き方でした。babel-preset-expo の変換結果を開発・本番で並べ、置き換わる書き方と残ってしまう書き方を表にしました。
⬡ 開発ツール2026-09-21
exit code 0 で止まるビルド — ログを黙らせていた設定と、空ファイルを通した存在チェック
eas build --local が exit code 0 but produced no further output で止まる件と、0 バイトのスタブが埋め込まれず dyld で落ちる件。どちらも原因は出力を減らす設定でした。自分のスクリプトを棚卸しする手順まで書き残します。