RORK LABEN
TOOLING — Rorkの開発者向けリポジトリが動き続けています。rork-xcodeが7月16日、rork-deviceが7月15日、rork-plistが7月13日に更新されましたOPUS46 — RorkでClaude Opus 4.6が稼働しています。Rork MaxはClaude Codeを土台にアプリを組み立てる設計ですSIM — ブラウザ上で動くクラウドのiOSシミュレータを備え、実機へのインストールは1クリック、App Storeへの提出は2クリックと案内されていますMAX — Rork MaxはReact Nativeではなく純粋なSwiftを出力します。iPhone・iPad・Apple Watch・Apple TV・Vision Pro、そしてiMessageまでが射程ですNATIVE — HealthKit、ARKitとLiDAR、NFC、Dynamic Island、Live Activities、Metalによる3D、Core MLのオンデバイス推論まで扱えますSEED — RorkはLeft Lane Capitalが主導する1,500万ドルのシードラウンドを実施し、Peak XVとa16z Speedrunが参加しましたTOOLING — Rorkの開発者向けリポジトリが動き続けています。rork-xcodeが7月16日、rork-deviceが7月15日、rork-plistが7月13日に更新されましたOPUS46 — RorkでClaude Opus 4.6が稼働しています。Rork MaxはClaude Codeを土台にアプリを組み立てる設計ですSIM — ブラウザ上で動くクラウドのiOSシミュレータを備え、実機へのインストールは1クリック、App Storeへの提出は2クリックと案内されていますMAX — Rork MaxはReact Nativeではなく純粋なSwiftを出力します。iPhone・iPad・Apple Watch・Apple TV・Vision Pro、そしてiMessageまでが射程ですNATIVE — HealthKit、ARKitとLiDAR、NFC、Dynamic Island、Live Activities、Metalによる3D、Core MLのオンデバイス推論まで扱えますSEED — RorkはLeft Lane Capitalが主導する1,500万ドルのシードラウンドを実施し、Peak XVとa16z Speedrunが参加しました
記事一覧/開発ツール
開発ツール/2026-04-27中級

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

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

Rork515Android43BackHandlerExpo Router5トラブルシューティング77

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つです。

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

複雑なモーダル制御で詰まった場合は 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-07-15
開発中は黄色い警告だけだったのに、復帰したら落ちた — ルートパラメータに関数を入れていた話
Rork が生成した画面遷移コードが、ルートパラメータに関数と Date を載せていました。開発中は警告で済むのに、アプリを復帰させると params.onFavorite is not a function で落ちます。原因と、ID だけを渡す形への直し方をまとめました。
開発ツール2026-05-19
Rork で AsyncStorage に大容量データを保存すると Android で CursorWindow エラーが出る原因と対処
Rork(React Native)で AsyncStorage に大きな JSON や画像メタデータを保存すると Android で発生する Row too big to fit into CursorWindow エラーの原因と、MMKV・SQLite・分割保存・圧縮など複数の現実的な対処をまとめています。
開発ツール2026-05-14
Android 13 以降で Rork アプリのプッシュ通知が届かない — POST_NOTIFICATIONS 権限の追加手順
Android 13(API 33)以降でプッシュ通知が届かない原因は、POST_NOTIFICATIONS 権限の追加漏れです。Rork 生成コードへの追加手順を Before/After で解説します。
📚RECOMMENDED BOOKS
大規模言語モデル入門
山田育矢
LLM開発
生成AIプロンプトエンジニアリング入門
我妻幸長
プロンプト
Claude CodeによるAI駆動開発入門
平川知秀
AI駆動開発
※ アフィリエイトリンクを含みます
もっと見る →