🙌

【Flutter】SnackBarが表示されないときの対処方法

2022/02/28に公開

はじめに

todoアプリを作っていて、todo編集画面から一覧画面に戻ってきた時に
Exception:Looking up a deactivated widget's ancestor is unsafe.〜が表示されSnackBarの表示ができずにはまったのでその解決方法をまとめてみました。

todoアプリ(動画内ではbooklist)やslidableのサンプルコードは以下動画を参考にしています。
今回のSnackBarについては(本題ではないですが)、こちらでもわかりやすく説明されてますので、参考にしてみてください。
https://www.youtube.com/watch?v=BY4x8FIj0PY

わかりづらい点等あるかと思いますが、どなたかの参考になればと思います。
また、もっといい方法や誤っている点等ございまいたらご指摘いただけますと幸いです。

flutter doctor

Doctor summary (to see all details, run flutter doctor -v):
[✓] Flutter (Channel stable, 2.10.0, on macOS 12.1 21C52 darwin-arm, locale ja-JP)
[✓] Android toolchain - develop for Android devices (Android SDK version 31.0.0)
[✓] Xcode - develop for iOS and macOS (Xcode 13.2.1)
[✓] Chrome - develop for the web
[✓] Android Studio (version 2020.3)
[✓] Connected device (2 available)
[✓] HTTP Host Availability

作っていたアプリの簡単な説明

  • main.dartでtodo一覧をListView、ListTile、Slidableを使って表示している。
  • ListTileのボタン(今回はSlaidableを使い、その中の編集ボタン)をタップするとNavigator.pushを使ってtodo編集画面(todo_edit_page.dart)に遷移する。
  • todo編集画面でtodo編集後、編集ボタンを押すとNavigator.popでmain.dartに戻ってくる。
  • main.dartに戻ったタイミングで「todoを更新しました」とSnackBarで表示する。

実際にエラーが出たコード(抜粋)

main.dartのSlidable内のonPressedでNavigator.pushした先(今回はtodo_edit_page.dart)からNavigator.popで戻ってきた時にSnackBarを表示するよう以下コードをかきました。

main.dart

class MainPage extends StatelessWidget {
  MainPage({Key? key}) : super(key: key);

  
  Widget build(BuildContext context) {
    return ChangeNotifierProvider<MainModel>(
      create: (_) => MainModel()..getTodoList(),
      child: Scaffold(
        appBar: ...
        body: Consumer<MainModel>(builder: (context, model, child) {
          final List<Todo> todoList = model.todoList;
          final List<Widget> widgets = todoList
              .map(
                (todo) => Slidable(
              endActionPane: ActionPane(
                motion: const ScrollMotion(),
                children: [
                  SlidableAction(
                    onPressed: (BuildContext context) async {
                      final String? updateTodoText = await Navigator.push(
                        context,
                        MaterialPageRoute(
                          builder: (context) => EditTodoPage(todo),
                        ),
                      );
                      // edit_todo_pageのNavigator.popで戻ったきた時のSnackBarを表示する処理
                      if (updateTodoText != null) {
                        SnackBar snackBar = const SnackBar(
                          backgroundColor: Colors.green,
                          content: Text('todoを更新しました!'),
                        );
                        // ここでexceptionを投げられる。
                        ScaffoldMessenger.of(context).showSnackBar(snackBar);
                      }
                      model.getTodoListRealtime();
                    },
                    ...
                  ),
                ],
              ),
              child: ListTile(
                title: Text(todo.title!),
              ),
            ),
          )
              .toList();
          return ListView(children: widgets);
        }),
        floatingActionButton:...
        }),
      ),
    );
  }

出てきたExceptionの詳細

Looking up a deactivated widget's ancestor is unsafe.
(訳)無効化されたウィジェットの祖先を検索することは安全ではありません。
description
At this point the state of the widget's element tree is no longer "
'stable.
(訳)この時点で、Widget の要素ツリーの状態は、もはや "
'stable'です。
hint
To safely refer to a widget's ancestor in its dispose() method, save a reference to the ancestor by calling dependOnInhzeritedWidgetOfExactType() in the widget's didChangeDependencies() method.
(訳)dispose() メソッドでウィジェットの祖先を安全に参照するには、ウィジェットの didChangeDependencies() メソッドで dependOnInhzeritedWidgetOfExactType() を呼び出し、祖先への参照を保存してください。

エラーの内容はモヤッとしか理解できないので、
ググったのですが、解決方法とか、詳細を書いてあるサイトを見つけられませんでした。
debugerで追いかけたところ、
ScaffoldMessenger.of(context)のcontextの値が
StatelessElement#00b48(DEFUNCT)(no widget)
と表示されていました。
おそらく、main.dartのScaffold等のWidgetが作られる前(つまり、 MainPageのWidgetがDEFUNCTのままの時)にScaffoldMessenger.of(context).showSnackBarを呼び出してしまい、その際のcontextが見つからないためと思われます。

解決した手順

結論から申し上げますと、
ScaffoldMessenger.of(context)でBuildContextを使ってshowSnackBar()を行うのではなく、
GlobalKeyをScaffoldMessengerに持たせ、GlobalKeyのcurrentStateからshowSnackBar()を行うと表示できる様になりました。

解決したコード(抜粋)

main.dart

class MainPage extends StatelessWidget {
  MainPage({Key? key}) : super(key: key);

  // ScaffoldMessengerに持たせるGlobalkeyを作っておく。
  final GlobalKey<ScaffoldMessengerState> _scaffoldKey =
      GlobalKey<ScaffoldMessengerState>();

  
  Widget build(BuildContext context) {
    return ChangeNotifierProvider<MainModel>(
      create: (_) => MainModel()..getTodoList(),
      // ScaffoldMessengerにkeyを持たせるため、ScaffoldをScaffoldMessengerでwrap
      child: ScaffoldMessenger(
        // 先ほどつくったGlobalKeyをここで指定しておく。
        key: _scaffoldKey,
        child: Scaffold(
          appBar: ...
          body: Consumer<MainModel>(builder: (context, model, child) {
            final List<Todo> todoList = model.todoList;
            final List<Widget> widgets = todoList
                .map(
                  (todo) => Slidable(
                    endActionPane: ActionPane(
                      motion: const ScrollMotion(),
                      children: [
                        SlidableAction(
                          onPressed: (BuildContext context) async {
                            final String? updateTodoText = await Navigator.push(
                              context,
                              MaterialPageRoute(
                                builder: (context) => EditTodoPage(todo),
                              ),
                            );
                            if (updateTodoText != null) {
                              SnackBar snackBar = const SnackBar(
                                backgroundColor: Colors.green,
                                content: Text('todoを更新しました!'),
                              );
                              // ScaffoldMessengerに持たせたlobalkeyのcurrentStateを使って
                              // showSnackBar()を実行
                              _scaffoldKey.currentState?.showSnackBar(snackBar);
                            }
                            model.getTodoListRealtime();
                          },
                          backgroundColor: Colors.grey,
                          foregroundColor: Colors.white,
                          icon: Icons.edit,
                          label: '編集',
                        ),
                      ],
                    ),
                    child: ListTile(
                      title: Text(todo.title!),
                    ),
                  ),
                )
                .toList();
            return ListView(children: widgets);
          }),
          floatingActionButton: ...
        ),
      ),
    );
  }
}

解決手順までの道のり

ScaffoldにKeyを持たせて、SnackBarを表示させる方法(つまり、BuildContextを使わずにSnackBarを表示する方法)を以下サイトで見つけました。

解決方法は、利用するScaffoldにid(key)を付けて、そのcurrentStateを使用することです。
https://qiita.com/ko2ic/items/f7bf98b4a30049027470

ただし、上記を行うと、現在は

'showSnackBar' is deprecated and shouldn't be used. Use ScaffoldMessenger.showSnackBar. This feature was deprecated after v1.23.0-14.0.pre.. (Documentation) Try replacing the use of the deprecated member with the replacement.

ScaffoldMessenger.showSnackBarを使う様にと言われます。

公式ドキュメントで、ScaffoldMessengerのことを詳しく書いてあるサイトがあったので、
そちらに書き方が載ってました。

The ScaffoldMessengercreates a scope in which all descendant Scaffolds register to receive SnackBars, which is how they persist across these transitions.
(訳)
ScaffoldMessengerは、すべての子孫ScaffoldがSnackBarを受け取るために登録するスコープを作成し、このトランジションでSnackBarを持続させます。

By instantiating your own ScaffoldMessenger, you can control which Scaffolds receive SnackBars, and which do not based on the context of your application.
(訳)
独自のScaffoldMessengerをインスタンス化することで、アプリケーションのコンテキストに応じて、どのScaffoldがSnackBarを受け取り、どのScaffoldが受け取らないかを制御することができます。
SnackBars managed by the ScaffoldMessenger | Flutter

具体的な書き方は同じサイトに書いてありました。

// If a ScaffoldMessenger.key is specified, the ScaffoldMessengerState can be directly
// accessed without first obtaining it from a BuildContext via
// ScaffoldMessenger.of. From the key, use the GlobalKey.currentState
// getter. This is used to manage SnackBars.
final GlobalKey<ScaffoldMessengerState> scaffoldMessengerKey = GlobalKey<ScaffoldMessengerState>();
ScaffoldMessenger(
  key: scaffoldMessengerKey,
  child: ...
)

あまり理解できていないのですが、

  • ScaffoldMessengerの子孫にScaffoldをおけば、その中にあるSnackBarをよしなに
    表示してくれる。
  • Scaffoldではなく、ScaffoldをwrapしたScaffoldMessengerにGlobalKeyを持たせる。
    ということのようです。
    それで上に掲載しました解決コードの様な記述になります。

終わりに

ここまで読んでいただきありがとうございました!
また参考にさせていただきましたサイトにも感謝いたします。

参考サイト

https://api.flutter.dev/flutter/material/SnackBar-class.html
https://qiita.com/ko2ic/items/f7bf98b4a30049027470
https://api.flutter.dev/flutter/material/ScaffoldMessenger-class.html
https://docs.flutter.dev/release/breaking-changes/scaffold-messenger

Discussion