【Flutter】SnackBarを上側に表示したい - プラグイン不使用/独自実装ほぼ無し

Flutterを採用しているプロジェクトで、「SnackBarを(デフォルトの下側ではなく) 上側に表示したい」という要件が上がった。

「SnackBar部品にプロパティを1つ渡すだけで簡単に実現出来るだろう」

そう思っていたが、意外と苦戦したので残しておく。

世の中にはリッチなSnackBar表示のためのプラグインが存在するが、それだとリッチすぎる(今回実現したいのは画面上部表示のみ)ため、導入を避けたかった。

Flutterのバージョンは3.24.3。
```
[√] Flutter (Channel stable, 3.24.3, on Microsoft Windows [Version 10.0.22631.4317], locale ja-JP)
```

## パターン1. Marginを使って上側に表示する

シンプルな実装で実現可能。下記の通り。

```dart
          ElevatedButton(
            onPressed: () {
              ScaffoldMessenger.of(context).showSnackBar(
                SnackBar(
                  behavior: SnackBarBehavior.floating,
                  showCloseIcon: true,
                  content: const Text("通常のSnackBar(上側)"),
                  margin: EdgeInsets.only(
                      bottom: MediaQuery.of(context).size.height - 100),
                ),
              );
            },
            child: const Text('Snackbar(Marginで上側へ調整)'),
          ),
```

SnackBar部品に対して、marginプロパティでbottom marginに画面サイズ分 - 100(調整分) を設定。

これでも一見OKなのだが、FloatingActionButtonやScaffoldのbottomNavigationBar等を使用して画面下部固定のWidgetを併用した場合に、SnackBarが画面上部にはみ出してしまうことがある。

画面下部に存在するWidgetの高さを取得できれば、marginの値をイイ感じに調整してはみ出しを防ぐことが出来るのではと思ったが、意外と苦労するようす。

## パターン2. MaterialBannerの活用して上側に表示する

MaterialBannerは、画面上部に固定的にユーザに情報を通知するためのエリアである。

https://api.flutter.dev/flutter/material/MaterialBanner-class.html

公式曰く、活用シーンとしてはこう。

> A banner displays an important, succinct message, and provides actions for users to address (or dismiss the banner).


よって、SnackBarとは利用意図が異なるのだが、今回のプロジェクトでは(将来を考えても)MaterialBannerを使用することは無いため問題は無い。

実装は下記。

```dart
          ElevatedButton(
            onPressed: () {
              ScaffoldMessenger.of(context).showMaterialBanner(
                MaterialBanner(
                  content: const Text(
                    'MaterialBannerです(上側表示のSnackbarの代わり)(コンテンツを全体的に押し下げる)',
                    style: TextStyle(color: Colors.white),
                  ),
                  backgroundColor: const Color(0xFF2B3030),
                  actions: [
                    // actionsに閉じるボタンを配置
                    CloseButton(
                      color: Colors.white,
                      onPressed: () => {
                        ScaffoldMessenger.of(context)
                            .hideCurrentMaterialBanner()
                      },
                    ),
                  ],
                ),
              );
            },
            child: const Text('SnackBar風のMaterialBanner(コンテンツを押し下げる)'),
          ),
```

SnackBarのようにScaffoldMessengerを活用してshow/hideを切り替える。

このようにメッセージが表示される。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/145371/209ff7b2-7a53-5322-0c8b-181edc7977a8.png)

デフォルトだと、メッセージが表示されると画面内のコンテンツが全体的に押し下げられる。

コンテンツにオーバレイするようにメッセージを表示したい場合は、`elevation` プロパティに値を設定すれば良い。(説明としては、「Z-indexを調整する値」とのこと)

ついでに、SnackBarのように自動で消えるようにしたいので下記のようにし、個人的には満足した。

```dart
          ElevatedButton(
            onPressed: () {
              ScaffoldMessenger.of(context).showMaterialBanner(
                MaterialBanner(
                  content: const Text(
                    'MaterialBannerです(上側表示のSnackbarの代わり)(コンテンツを押し下げない)(勝手に消える)',
                    style: TextStyle(color: Colors.white),
                  ),
                  elevation: 1,
                  backgroundColor: const Color(0xFF2B3030),
                  actions: [
                    // actionsに閉じるボタンを配置
                    CloseButton(
                      color: Colors.white,
                      onPressed: () => {
                        ScaffoldMessenger.of(context)
                            .hideCurrentMaterialBanner()
                      },
                    ),
                  ],
                  onVisible: () => Future.delayed(
                      const Duration(seconds: 5),
                      () => {
                            if (context.mounted)
                              ScaffoldMessenger.of(context)
                                  .hideCurrentMaterialBanner(),
                          }),
                ),
              );
            },
            child: const Text('SnackBar風のMaterialBanner(コンテンツに重ねる&勝手に消える)'),
          ),
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/145371/16f30966-e386-f98a-eb4c-caee4d1b03bb.png)

onVisibleのタイミングで`Future.delayed`を仕込み、その中で`hideCurrentMaterialBanner`する。

なお、この時にcontextが失われている可能性がある点に注意(ex: 画面遷移)

画面を跨いでも使用したい場合は、NavigatorStateのGlobalKeyを作成しておき、そのcontextを取りまわすようにすれば良い。

```dart
/// GlobalKeyの宣言(アプリ内で唯一のGlobalKey)
final GlobalKey<NavigatorState> globalNavigatorKey = GlobalKey<>(NavigatorState);

...

/// GlobalKeyベースのcontextを利用する側
final context = globalNavigatorKey.currentContext!;

...

/// 上記のcontextを利用すれば画面遷移してもOK
ScaffoldMessenger.of(context).hideCurrentMaterialBanner()
```

非同期処理が挟まる場合はcontextのハンドリングには気を付けないといけない。