【Flutter】バグを生み出してしまうcontextの使い方

<ul>
<li><a href="#h1">1.前書き</a></li>
<li><a href="#h2">2.BottomNavigationを使用した際のモーダル</a></li>
<li><a href="#h3">3.contextが変わってしまう問題</a></li>
</ul>

<h1 id="h1">1.前書き</h1>
何気なくcontextを使ってしまうと引き起こしてしまう
バグや、挙動の違いなどについて紹介しています。
contextについてあまり分からないと言った方は<br>
<a href = "https://qiita.com/YutaMatsuura715/items/32e4f45b497f8b759e80">Flutterのcontextとは何者か。</a><br>
の記事で解説してます。<br>

<h1 id="h2">2.BottomNavigationを使用した際のモーダル</h1>
ここにサンプルアプリがあります。<br>
内容は至って単純で<br>
①BottomNavigation機能<br>
②モーダルの表示<br>
のみです。<br>
画面内の「push」ボタンを押すと下からモーダルが表示されます。<br>
モーダルを表示させる際に
「context」を引数としてonPressed関数に渡して<br>showModalBottomSheetに渡しています。<br>
この際に、useRootNavigatorをfalseにするかtrueにするかで挙動が変わります。<br>
<br>
<table class="table1">
  <tr><th>useRootNavigator</th><th>true</th><th>false</th>
  <tr><td>挙動</td>
<td><img width="200" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1276686/76f91f31-8159-4c12-2b34-7f728142cb77.gif"></td>
<td><img width="200" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1276686/887be913-6fc7-deb3-34ba-ad3290748e31.gif"></td><tr>
</table>

以下はソースコードになります。
```dart
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';

void main() => runApp(const MyApp());

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  void onPressed(BuildContext context) async {
    await showModalBottomSheet(
      useRootNavigator: false, //←useRootNavigatorがtrueかfalseで挙動が変わる
      context: context,
      builder: (context) => Column(
        mainAxisSize: MainAxisSize.min,
        children: <Widget>[
          ListTile(
            title: const Text('POP'),
            onTap: () => Navigator.of(context).pop('delete'),
          ),
          const Divider(),
          ListTile(
            title: const Text('POP'),
            onTap: () => Navigator.of(context).pop('delete'),
          ),
        ],
      ),
    );
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
        home: CupertinoTabScaffold(
            tabBar: CupertinoTabBar(items: const [
              BottomNavigationBarItem(
                icon: Icon(Icons.music_note),
              ),
              BottomNavigationBarItem(
                icon: Icon(Icons.settings),
              ),
            ]),
            tabBuilder: (context, index) {
              switch (index) {
                case 0:
                  return CupertinoTabView(
                    builder: (context) {
                      return CupertinoPageScaffold(
                        child: Center(
                          child: ElevatedButton(
                            child: const Text('push'),
                            onPressed: () => onPressed(context),
                          ),
                        ),
                      );
                    },
                  );
                case 1:
                  return CupertinoTabView(
                    builder: (context) {
                      return const CupertinoPageScaffold(
                        child: Center(child: Text('tab 2')),
                      );
                    },
                  );
              }
              return Container();
            }));
  }
}

```
userootNavigatorがtrueとfalseの場合では
画面遷移を管理するNavigatorStateの取得先が異なります。
正確に言うと、
userootNavigatorがtrueの場合は
trueの場合はfindRootAncestorStateOfTypeメソッド
falseの場合はfindAncestorStateOfTypeメソッドが呼ばれる。
それぞれのメソッドの違いが親のたどり方になります。
分かりやすく図解で説明します。
(<a href="https://qiita.com/YutaMatsuura715/items/32e4f45b497f8b759e80">こちらの記事ではコードベースで詳細を追っています。</a>)

<br>
<table class="table1">
  <tr><th>useRootNavigator</th><th>navigatorState取得の動き</th>
  </tr>
<tr><td><strong>true</strong></td>
<td><img width="500" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1276686/4c975c82-c0d7-71c1-a789-d1f361781656.png"></td>
</tr>
  <tr><td><strong>false</strong></td>
<td><img width="500" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1276686/f90fe5fd-9a4f-d1f2-8771-238054f25ab4.png"></td>
</table>

Flutterではデフォルトで画面遷移を管理するNavigatorStateが用意されており
BottomNavigationなどの機能を追加した際は
NavigatorStateがネストされている状態となります。
デフォルトで画面遷移を管理するNavigatorStateに対して操作したい場合は
useRootNavigatorをtrueにすることで
画面全体に対してモーダルなどを被せることが出来ます。
逆にfalseの場合は、BottomNavigationが管理するNavigationStateを操作しているので
BottomNavigationで管理している内側の画面にのみモーダルが表示されます。
ここは、理解していないと意図しないバグを生み出す可能性があるのでしっかり理解しておきましょう！

<h1 id="h3">3.contextが変わってしまう問題</h1>
これは間違った再帰処理の方法によりcontextが変わってしまう問題。<br>
ユーザ登録処理でユーザはエラーの失敗により3回までリトライ処理を行うことが出来るとする。<br>
まず、<br>
1.OnTappedResisterButtonにcontextを渡す。<br>
2,3.ユーザ登録処理に失敗<br>
4.ダイアログの【OK】ボタンタップ時などに実行する関数にOnTappedResisterButtonを渡す。<br>
　この際に渡すcontextは【context_1】<br>
5.ユーザはダイアログの【OK】ボタンをタップして再度ユーザ登録処理を行う<br>
6.その際に、一回目の登録処理では行われなかった処理がifなどで実行され　<br>
　stateが更新されpresentation側でリビルドが走る<br>
　この際に、context_1の生成元であるwidgetにリビルドがかかり、<br>
　widgetが持つcontextがcontext_2になる<br>
7,8.ユーザ登録処理に失敗<br>
9.エラーダイアログ表示<br>

**最後に表示されるダイアログがPOPする際に使用するcontextが【context_1】の場合、presentationではそんなcontextのwidgetはどこにもな！と言われのPOP時にエラーが発生してしまう。**

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1276686/07702f81-6e2f-86f4-e67d-d7d05bfa585a.png)