【Flutter】通知を制御!ScaffoldMessengerとScaffoldMessengerState

対象者

  • Flutterを使用してモバイルアプリケーションの開発に携わっている方
  • UIのユーザーエクスペリエンスを向上させたいと考えている方
  • 新しい技術を学び、キャリアアップを目指している開発者の方

はじめに

FlutterのScaffoldMessengerウィジェットは、アプリケーション全体でSnackBarなど一時的なウィジェットを表示します。これにより、画面遷移があってもメッセージが保持されるなど、以前のScaffoldに依存した方法よりも柔軟なメッセージングが可能になります。また、ScaffoldMessageで例外が発生して、スナックバーが表示されない時の対応法も記載します。

ScaffoldMessengerの基本

ScaffoldMessengerとは何か?

ScaffoldMessengerは、Flutterアプリケーションにおいて、SnackBarやMaterial Bannerなどの通知を管理するためのウィジェットです。これは画面遷移があっても通知を維持することができる非常に便利な機能を提供します。
ScaffoldMessengerは全てのScaffoldウィジェットがSnackBarイベントを受け取るために登録するスコープを作成します。これにより、ユーザーが新しい画面に遷移した後も、SnackBarメッセージが表示され続けることが可能になります。

ScaffoldMessengerの使い方

SnackBarの表示方法

ScaffoldMessengerを使用してSnackBarを表示する方法は、Flutter開発の中で非常に一般的なタスクです。ScaffoldMessenger.of(context).showSnackBarメソッドを呼び出すことで、簡単にSnackBarを表示できます。

ユーザーがアクションを完了した際にフィードバックを提供するために以下のようなコードが使用されます。

ScaffoldMessenger.of(context).showSnackBar(
  SnackBar(content: Text('アクションが成功しました'))
);

このコードは、ユーザーが何らかのアクションを成功させたことを通知するためにSnackBarを表示する典型的な例です。SnackBarの表示方法は、ユーザーに対して直感的でタイムリーなフィードバックを提供するための効果的な手段です。

SnackBarのカスタマイズ

SnackBarのカスタマイズは、アプリケーションのブランディングとユーザー体験を向上させるために重要です。FlutterではSnackBarの外観や挙動を簡単にカスタマイズできます。これは、SnackBarのAPIが様々なカスタマイズオプションを提供しているためです。詳しい使い方は、以下の記事をご参照ください。

【Flutter】メッセージ提示に必須!SnackBar入門

【Flutter】Snackbar(スナックバー)をちょっとお洒落にする

FlutterでのScaffoldMessengerの使用と、SnackBarが表示されない問題の解決方法について、以下のようにまとめることができます。

ScaffoldMessengerが使えないとき

ScaffoldMessengerとSnackBarの表示

FlutterのScaffoldMessengerウィジェットは、アプリケーション内のSnackBarメッセージを管理するために使用されます。これは、画面遷移があった場合でもSnackBarが継続して表示されるようにするためのものです。しかし、Navigator.popを使用して画面遷移を行った後にSnackBarを表示しようとすると、Looking up a deactivated widget's ancestor is unsafe.というエラーが発生することがあります。これは、BuildContextがもはや有効でないため、SnackBarを表示しようとした際に参照できるScaffoldが存在しないことを意味します。

問題の理由

この問題は、Navigator.popが実行された後、BuildContextが古くなっている(すでに画面が非アクティブになっている)ために発生します。ScaffoldMessenger.of(context)を呼び出すと、Flutterは現在のBuildContextに関連付けられているScaffoldを探しますが、その時点でBuildContextはもはや有効ではないため、Scaffoldを見つけることができず、エラーが発生します。

解決方法

この問題を解決するためには、GlobalKey<ScaffoldMessengerState>を使用してScaffoldMessengerに直接アクセスする方法があります。GlobalKeyを使用すると、BuildContextに依存せずにScaffoldMessengercurrentStateを取得し、SnackBarを表示することができます。

以下は、GlobalKeyを使用してSnackBarを表示する方法のサンプルコードです。

final GlobalKey<ScaffoldMessengerState> scaffoldMessengerKey = GlobalKey<ScaffoldMessengerState>();

// ...

ScaffoldMessenger(
  key: scaffoldMessengerKey,
  child: Scaffold(
    // ...
  ),
);

// ...

// 画面遷移後にSnackBarを表示
SnackBar snackBar = SnackBar(
  content: Text('画面遷移中'),
);
scaffoldMessengerKey.currentState?.showSnackBar(snackBar);

この方法を使用することで、画面遷移後も安全にSnackBarを表示することができます。また、ScaffoldMessengerkeyを設定することで、どのScaffoldSnackBarを受け取るかを制御することも可能になります。これにより、画面遷移があった後でもSnackBarを表示することができ、Looking up a deactivated widget's ancestor is unsafe.というエラーを回避することができます。

エラー詳細

 [ERROR:flutter/runtime/dart_vm_initializer.cc(41)] Unhandled Exception: Looking up a deactivated widget's ancestor is unsafe.
E/flutter ( 5027): At this point the state of the widget's element tree is no longer stable.
E/flutter ( 5027): To safely refer to a widget's ancestor in its dispose() method, save a reference to the ancestor by calling dependOnInheritedWidgetOfExactType() in the widget's didChangeDependencies() method.

Q&A

Q1: ScaffoldMessengerとは具体的にどのようなウィジェットですか?

A1: ScaffoldMessengerはFlutterのウィジェットで、SnackBarやBottomSheetなどの一時的なUI要素をアプリケーション全体で管理するために使用されます。これにより、画面遷移があってもメッセージが維持されるなど、ユーザーに一貫した体験を提供することができます。

Q2: ScaffoldMessengerでよくあるエラーとその解決策は何ですか?

A2: よくあるエラーの一つに、不適切なコンテキストを使用してSnackBarを表示しようとした場合があります。これは、ScaffoldMessenger.of(context)を呼び出す際に、ScaffoldMessengerのスコープ外のコンテキストを使用していることが原因です。正しいコンテキストで使用するか、ScaffoldMessengerStateを使用する方法を検討してください。

Q3: ScaffoldMessagerを使用しないでスナックバーをどうやって出しますか

ScaffoldMessengerStateを使用した方法があります。
かつてはScaffold.of(context).showSnackBar(snackBar)がありましたが、消えたなぁ、、

まとめ

FlutterのScaffoldMessengerウィジェットについて学び、その基本から応用、トラブルシューティングまで理解しました。

参考

ソース(main.dartにコピペして動作確認用)

import 'package:flutter/material.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  // GlobalKeyを作成します。これを使用してScaffoldMessengerにアクセスします。
  final GlobalKey<ScaffoldMessengerState> scaffoldMessengerKey =
      GlobalKey<ScaffoldMessengerState>();

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      scaffoldMessengerKey:
          scaffoldMessengerKey, // MaterialAppにGlobalKeyを設定します。
      theme: ThemeData(useMaterial3: true),
      home: MyHomePage(scaffoldMessengerKey: scaffoldMessengerKey),
    );
  }
}

class MyHomePage extends StatelessWidget {
  final GlobalKey<ScaffoldMessengerState> scaffoldMessengerKey;

  MyHomePage({required this.scaffoldMessengerKey});

  void _navigateToNextPage(BuildContext context) {
    Navigator.of(context).push(MaterialPageRoute(
        builder: (context) => NextPage(scaffoldMessengerKey)));
  }

  final _snackBar = SnackBar(content: Text('次の画面に遷移中'));

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('ScaffoldMessenger Demo'),
      ),
      body: Center(
        child: Column(
          children: [
            FilledButton(
              onPressed: () {
                ScaffoldMessenger.of(context).showSnackBar(_snackBar);
                Future.delayed(
                    Duration(seconds: 1), () => _navigateToNextPage(context));
              },
              child: Text('ScaffoldMessenger'),
            ),
            FilledButton(
              onPressed: () {
                scaffoldMessengerKey.currentState?.showSnackBar(_snackBar);
                Future.delayed(
                    Duration(seconds: 1), () => _navigateToNextPage(context));
              },
              child: Text('ScaffoldMessengerState'),
            ),
          ],
        ),
      ),
    );
  }
}

class NextPage extends StatelessWidget {
  final _snackBar = SnackBar(content: Text('前の画面に遷移中'));
  final GlobalKey<ScaffoldMessengerState> scaffoldMessengerKey;

  NextPage(this.scaffoldMessengerKey);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Next Page'),
      ),
      body: Center(
        child: Column(
          children: [
            Text('Welcome to the next page!'),
            FilledButton(
                onPressed: () {
                  Navigator.of(context).pop();
                  Future.delayed(
                      Duration(seconds: 1),
                      // 例外が発生する
                      () => ScaffoldMessenger.of(context)
                          .showSnackBar(_snackBar));
                },
                child: Text('back with ScaffoldMessenger(エラー発生)')),
            FilledButton(
                onPressed: () {
                  Navigator.of(context).pop();
                  Future.delayed(
                      Duration(seconds: 1),
                      () => scaffoldMessengerKey.currentState
                          ?.showSnackBar(_snackBar));
                },
                child: Text('back with scaffoldMessengerKey')),
          ],
        ),
      ),
    );
  }
}

ScaffoldMessengerの利用

上記のソースコードでは、ScaffoldMessengerを使用してSnackBarを表示する基本的な方法を示しています。MyHomePageクラス内で、FilledButtonウィジェットのonPressedコールバックにて、ScaffoldMessenger.of(context).showSnackBar(_snackBar)を呼び出すことで、SnackBarを表示しています。これは、現在のBuildContextに関連付けられた最も近いScaffoldMessengerを見つけ出し、その上でSnackBarを表示する標準的な方法です。

GlobalKeyの利用

一方で、GlobalKey<ScaffoldMessengerState>を使用する方法もあります。これは、ScaffoldMessengerに一意のグローバルキーを割り当てることで、アプリケーションのどの部分からでも同じScaffoldMessengerにアクセスできるようにするものです。MyAppクラスでGlobalKeyを作成し、MaterialAppScaffoldMessengerKeyプロパティに設定しています。これにより、ScaffoldMessengerの状態をアプリケーション全体で共有できるようになります。

画面遷移とSnackBarの表示

NextPageクラスでは、画面遷移後にSnackBarを表示するために、GlobalKeyを使用しています。Navigator.of(context).pop()を呼び出した後、Future.delayedを使用して遅延を設け、その後でGlobalKeyを介してSnackBarを表示しています。これは、画面遷移が完了してからSnackBarを表示するために必要です。BuildContextが古くなることを防ぐために、GlobalKeyを使用して直接ScaffoldMessengerにアクセスしています。

エラーの回避

NextPageクラスの最初のボタンでは、Navigator.of(context).pop()の後にScaffoldMessenger.of(context)を使用してSnackBarを表示しようとしていますが、これはエラーを引き起こします。これは、pop()が呼び出された後、contextがもはや有効でないためです。GlobalKeyを使用することで、この問題を回避し、安全にSnackBarを表示することができます。