FlutterでWillPopScopeからPopScopeへの移行方法
問題の背景
WillPopScope
ウィジェットは、Flutterアプリで物理的な戻るボタン(Android)やスワイプジェスチャによるナビゲーションをカスタマイズするために長く使用されてきました。しかし、Flutterバージョン3.12.0-1.0.pre以降、このウィジェットは**非推奨(deprecated)**となりました。代わりにPopScope
ウィジェットの使用が推奨されています。
非推奨の警告が表示される典型的なコード例:
WillPopScope(
onWillPop: () async {
// カスタムロジック
return false; // 戻るナビゲーションをブロック
},
child: Scaffold(...),
)
この変更の主な理由は:
- 新しいAndroidの予測可能な戻るナビゲーションとの互換性向上
- API設計の簡素化とエラーの削減
- ナビゲーション結果の扱いをより直感的に
基本移行方法
WillPopScope
の代わりにPopScope
を使用する基本的な移行パターン:
PopScope(
canPop: false, // 戻る操作を制御するフラグ
onPopInvokedWithResult: (didPop, result) {
if (!didPop) {
// カスタムロジックを実行
// 戻る操作を許可する場合はNavigator.pop()を呼び出す
}
},
child: Scaffold(...),
)
主要プロパティの説明
canPop
:false
に設定すると、戻る操作がデフォルトでブロックされるtrue
の場合は標準の戻る動作が実行される
onPopInvokedWithResult
:didPop
: 実際にポップ操作が発生したかどうかを示すresult
: ナビゲーターから返される結果データ(Navigator.pop(result)
で設定)
実践的な移行例
ケース1: 戻る操作を完全にブロック
戻るボタンを無効化するシンプルな例:
PopScope(
canPop: false,
onPopInvokedWithResult: (didPop, result) {},
child: Scaffold(...),
)
ケース2: 確認ダイアログを表示
戻る前にユーザー確認を求める実装:
Future<bool> _showConfirmDialog(BuildContext context) async {
return await showDialog<bool>(
context: context,
builder: (context) => AlertDialog(
title: Text('確認'),
content: Text('本当に終了しますか?'),
actions: [
TextButton(
onPressed: () => Navigator.pop(context, false),
child: Text('キャンセル'),
),
TextButton(
onPressed: () => Navigator.pop(context, true),
child: Text('終了'),
),
],
),
) ?? false;
}
@override
Widget build(BuildContext context) {
return PopScope(
canPop: false,
onPopInvokedWithResult: (didPop, result) async {
if (didPop) return;
final shouldPop = await _showConfirmDialog(context);
if (shouldPop && context.mounted) {
Navigator.pop(context); // ページを閉じる
}
},
child: Scaffold(...),
);
}
重要
onPopInvokedWithResult
内でcontext
を使用する場合、非同期処理中の画面破棄を考慮してcontext.mounted
チェックを必ず行いましょう
ケース3: WebViewでの戻る操作制御
WebView内のナビゲーション履歴を扱う例:
final InAppWebViewController webViewController;
PopScope(
canPop: false,
onPopInvokedWithResult: (didPop, result) async {
if (await webViewController.canGoBack()) {
webViewController.goBack(); // WebView内で戻る
} else if (context.mounted) {
Navigator.pop(context); // WebViewを閉じる
}
},
child: WebViewWidget(...),
)
高度な使用法
ダブルタップでアプリ終了
戻るボタン2回タップでアプリ終了するパターン:
class DoubleTapExit extends StatefulWidget {
const DoubleTapExit({super.key});
@override
State<DoubleTapExit> createState() => _DoubleTapExitState();
}
class _DoubleTapExitState extends State<DoubleTapExit> {
DateTime? lastBackPressedTime;
@override
Widget build(BuildContext context) {
return PopScope(
canPop: false,
onPopInvokedWithResult: (didPop, result) {
if (didPop) return;
final now = DateTime.now();
final gap = lastBackPressedTime == null
? const Duration(hours: 1)
: now.difference(lastBackPressedTime!);
if (gap < const Duration(seconds: 2)) {
SystemNavigator.pop(); // アプリ終了
} else {
lastBackPressedTime = now;
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('もう一度タップで終了'),
duration: Duration(seconds: 2),
),
);
}
},
child: Scaffold(...),
);
}
}
注意点とベストプラクティス
バージョン互換性
PopScope
はFlutter 3.7以降で利用可能- Flutter 3.16以降では
onPopInvoked
が非推奨となり、onPopInvokedWithResult
に完全移行
間違った実装
以下のような非推奨APIの使用は避けましょう:
// 非推奨: 古いPopScopeの使用法
PopScope(
onPopInvoked: (didPop) {} // Flutter 3.16以降では非推奨
)
// 非推奨: WillPopScopeは完全に廃止
WillPopScope(...)
公式ドキュメント
詳細は公式移行ガイドを参照: Migrating from WillPopScope to PopScope
まとめ
WillPopScope
からPopScope
への移行は必須canPop
とonPopInvokedWithResult
の組み合わせで柔軟な制御が可能- ダイアログ表示やWebView制御など、高度なユースケースも対応可能
- コンテキストの生存状態(
mounted
)チェックを忘れずに - 常に最新のFlutterバージョンと公式ドキュメントを参照
新しいPopScope
APIは予測可能なナビゲーションを実現し、Androidの最新機能との互換性を向上させます。移行作業は現在のコードベース規模に関わらず、比較的シンプルに行えるでしょう。