どうも、株式会社YOUTRUSTのアプリ開発のリードエンジニアを務めているashdikこと朝日(YOUTRUST / X)です。
最近は、またスマブラ熱が少し再燃しておりクロムをVIP入りさせようと尽力させているところです。
はじめに
Flutter 3.32にアップデートした際、NavigatorState.push を使った画面遷移で予期しない挙動を発見しました。
await Navigator.of(context).push(...); await something(); // 画面から戻った時だけ実行されることを期待していた
僕の認識では、このコードは「遷移先から何らかの方法でこの画面に戻ってきた時に something が実行される」。
だったのですが、something() が違うパターンでも実行されるようになっていました。
そんなお話です。
前提
go_routerは使用しておらず、MaterialPageRoute/pushで画面遷移を実現Navigator 1.0を採用
問題の詳細
アプリでのNavigator Stack
MaterialApp (root)
├── 起動画面
│
├── 新規登録/ログイン選択画面
├──── 画面A // ターゲット
└──── 画面B // ターゲット
│
├── チュートリアル画面
│ ├──── チュートリアル項目入力画面 * n個
│ └──── ...
│
└── ホーム画面
起動画面やホーム画面などは rootNavigatorによる pushReplacementや pushAndRemoveUntil、 画面Aやチュートリアル項目入力画面は 通常の NavigatorState.push を使って遷移しています。
今回のターゲットは画面Aや画面Bです。
従来の挙動(~Flutter 3.31)
// 画面Aでの実装 await Navigator.of(context).push(画面B); await something();
従来の実行順序:
1. push により画面遷移
2. ユーザーが戻る(pop)まで処理は中断
3. 画面Bから戻った直後にsomethingが実行
4. チュートリアルが終わり、ホーム画面に遷移しても実行されない
👉 await push は「遷移先の画面が閉じるまで待つ」挙動だった。
新しい挙動(Flutter 3.32~)
新しい実行順序:
- push により画面遷移
- ユーザーが戻る(pop)まで処理は中断
- 画面Bから戻った直後に
somethingが実行 - チュートリアルが終わり、ホーム画面に遷移した時にsomethingが実行されてしまう
背景: 公式の変更について
Breaking Change の詳細
Flutter リポジトリで Navigator に関するものを調査してみると以下のIssueが見つかります。
pop の代わりに removeRoute を呼ぶ必要がある場合があるが、そうすると await による Future が終了しない、という旨の記述が見つかります。
そして、最後のコメントで述べられているように、下記PRで修正され、 Flutter 3.32.x から実現されました。
どうやら、 markForRemove ではなく markForComplete が実行される様になったみたいですね。
つまり、 元々の挙動は想定されていた挙動ではなく不具合によるものだったことが垣間見れました。
対処法
それでは、弊社のアプリでの対処法を書き記していこうと思います。
オンボーディングを rootNavigator で遷移しない
先ほど述べた形から、
MaterialApp (root)
├── 起動画面
│
├── 新規登録/ログイン選択画面
├──── 画面A // ターゲット
└──── 画面B // ターゲット
├── チュートリアル画面
│ ├──── チュートリアル項目入力画面 * n個
│
└── ホーム画面
の様なNavigation Stackに変更します。
終了時に pushAndRemoveUntil の代わりに pushReplacement に変更する
// Before Navigator.of(context, rootNavigator: true).pushAndRemoveUntil(...); // After Navigator.of(context, rootNavigator: true).pushReplacement(...);
実はこの段階で、ほとんどのエラーは直っていました。
まだ調査し切れていないのですが、残っている問題は以下のどれかを採用することで直ります(一部、想定もあり)。
1. 返却値を見ること
final result = await Navigator.of(context).push(...); if (result != null) { something(); }
2. context.mounted && !Navigator.of(context).canPop() を見ること
await Navigator.of(context).push(...); if (context.mounted && !Navigator.of(context).canPop()) { something(); }
弊社はこのパターンを採用しましたが、必ずしもこれで上手くいかない場合もあると思います。
3. NavigatorObserverを用い、didPopによる判定を行う
弊社の場合、 push の後にやりたいのは全て共通でした。
なので、その処理を NavigatorObserver の didPop メソッドを override することで実現できると考えています。
(未実装)
class NavigationReturnObserver extends NavigatorObserver { ... @override void didPop(Route route, Route? previousRoute) { super.didPop(route, previousRoute); // something(); } }
まとめ
Flutter 3.32での NavigatorState.push をawaitすることによる挙動の変更とその対処についてまとめました。
一部、まだ完全に理解/調査し切れていない部分があるのでその部分に関しては個人Xや今後のブログで掲載できればと思っております。
誤りがあった場合は指摘いただければ変更させていただきます。
Flutterはアップデートのたびに、処理や描画が高速化していたり早くアップデートしたくなりますよね。
でも、その一方でこのように破壊的変更がサラッとあったりするので確認も入念に行う様にしましょう。
最後に
弊社のアプリエンジニアは、Flutterが大好きな人ばかりです。 そんな我々とFlutterを極めたい方は、ぜひカジュアル面談からでもOKなのでお待ちしております。