[Flutter] Introducing the Overlay class to display notification messages

This article can be read in about 19 minutes.

Introduction

Previously, in this article, I described how to deal with RenderFlex overflowed errors.

This time, as a permanent feature, 「Overlay I would like to use a class to display a new widget layer on top of other widgets.

By the way, currently any message displayed on the app’s home screen is displayed as a Text widget at the top of the screen.

Introduction of overlay_support package

There is a package called overlay_support that allows you to easily create toasts and in-app notifications, so I decided to use it.

overlay_support | Flutter package
provider support for overlay, easy to build toast and internal notification
ShellScript
$ flutter pub add overlay_support
$ flutter pub get

Wrap AppWidget with OverlaySupport

OverlaySupport.global()To use it in the main part of the app, main.dartwrite as follows.

Dart
return OverlaySupport.global(child: MaterialApp());

Fixed message display class

This is the class that was originally created to display a Text widget at the top of the screen.

Messages can be closed using the close icon on the right, and a show dialog will open when you click on the message bar. Replace this with the current Overlay_support.

Dart

Dart
// lib/widgets/top_message_bar.dart
import 'package:flutter/material.dart';
import 'package:overlay_support/overlay_support.dart';

void showCustomMessageBar({
  required BuildContext context,
  required String message,
  required VoidCallback onClose,
  required VoidCallback onTapped,
  Duration? duration,
}) {
  showOverlayNotification(
    (context) {
      return GestureDetector(
        onTap: () {
          onTapped();
          OverlaySupportEntry.of(context)?.dismiss();
        },
        child: Container(
          padding: const EdgeInsets.symmetric(
            vertical: 10,
            horizontal: 16,
          ),
          margin: const EdgeInsets.fromLTRB(16, 60, 16, 0),
          decoration: BoxDecoration(
            color: Colors.blue.shade100,
            borderRadius: BorderRadius.circular(10),
            boxShadow: const [
              BoxShadow(
                color: Colors.black26,
                blurRadius: 10,
                spreadRadius: 1,
                offset: Offset(0, 5),
              ),
            ],
          ),
          child: Row(
            mainAxisSize: MainAxisSize.max,
            children: [
              Expanded(
                child: Text(
                  message,
                  style: const TextStyle(
                    fontSize: 14,
                    color: Colors.black,
                    decoration: TextDecoration.none,
                  ),
                ),
              ),
              IconButton(
                icon: const Icon(Icons.close),
                onPressed: () {
                  OverlaySupportEntry.of(context)?.dismiss();
                  onClose();
                },
              ),
            ],
          ),
        ),
      );
    },
    duration: duration,
  );
}

Here is the code after the change.

durationMake the parameter optional so that the caller can set the display period for each notification.

OverlaySupportEntry.of(context)?.dismiss();is a method that removes the visible overlay widget (in this case the message bar). This allows overlays to be closed programmatically when the user presses the close button or taps the message bar itself.

Dart

Dart
// lib/widgets/top_message_bar.dart
import 'package:flutter/material.dart';
import 'package:overlay_support/overlay_support.dart';

void showCustomMessageBar({
  required BuildContext context,
  required String message,
  required VoidCallback onClose,
  required VoidCallback onTapped,
  Duration? duration, // オプショナルにして、デフォルトはnull(無期限表示)
}) {
  showOverlayNotification((context) {
    return GestureDetector(
      onTap: () {
        onTapped();
        OverlaySupportEntry.of(context)?.dismiss();
      },
      child: Container(
        padding: const EdgeInsets.all(10),
        margin: const EdgeInsets.symmetric(horizontal: 10),
        decoration: BoxDecoration(
          color: Colors.blue.shade100,
          borderRadius: BorderRadius.circular(10),
          boxShadow: [
            BoxShadow(
              color: Colors.black26,
              blurRadius: 10,
              spreadRadius: 1,
              offset: Offset(0, 5),
            ),
          ],
        ),
        child: Row(
          mainAxisSize: MainAxisSize.max,
          children: [
            Expanded(
              child: Text(
                message,
                style: const TextStyle(fontSize: 14),
              ),
            ),
            IconButton(
              icon: const Icon(Icons.close),
              onPressed: () {
                OverlaySupportEntry.of(context)?.dismiss();
                onClose();
              },
            ),
          ],
        ),
      ),
    );
  }, duration: duration); // デフォルトではdurationはnull
}

Calling showCustomMessageBar

_HomeTabStateAdd firmware check and message bar display logic to.

If you specify Duration.zero for the duration parameter, the message will be displayed permanently. It can be closed by clicking the close icon.

Dart
// lib/screens/home/home_tab.dart
@override
void initState() {
  super.initState();
  _checkFirmwareUpdate();
}

void _checkFirmwareUpdate() async {
  final firmwareUpdate = ref.watch(firmwareUpdateProvider);
  firmwareUpdate.whenData((response) {
    if (response.message == "NEW_FIRMWARE_AVAILABLE") {
      showCustomMessageBar(
        context: context,
        message: "新しいファームウェアバージョン(${response.newFirmwareVersion})がダウンロード可能です。",
        onClose: () {},
        onTapped: () {
          showDialog(
            context: context,
            builder: (BuildContext context) {
              return FirmwareUpdateDialog(
                newVersion: response.newFirmwareVersion ?? "",
                onConfirm: () {
                  final userNotifier = ref.read(userProvider.notifier);
                  userNotifier.updateUser(User(firmwareVersion: response.newFirmwareVersion));
                },
                onCancel: () {},
              );
            },
          );
        },
        duration: Duration.zero;
      );
    }
  });
}

However, when building this, the following error occurs.

visitChildElements()An error means the method cannot be called because the list of child elements in the widget tree is still being updated during the build.

Overlay is a feature that dynamically adds new widgets (such as dialogs and toast notifications) on top of an independent widget tree. If you manipulate the overlay directly within the build method, it will try to insert the new widget into the widget tree before the build process is complete, so the framework will think that “operations on child elements are occurring during the build.” , causes an error.

To solve this problem, the code that manipulates the overlay must be executed after the build process is complete, and to do so, the code that manipulates the overlay must be wrapped.WidgetsBinding.instance.addPostFrameCallback

Dart
WidgetsBinding.instance.addPostFrameCallback((_) {
  showOverlay(...);  // Overlayを表示する関数
});

What is WidgetsBinding.instance.addPostFrameCallback?

WidgetsBinding.instance.addPostFrameCallbackis a method used within the Flutter framework to schedule a callback (subsequent processing) to be executed after a frame of screen drawing is completed. This callback is executed after the frame has finished drawing, so you can safely display the overlay.

addPostFrameCallback method - SchedulerBinding mixin - scheduler library - Dart API
API docs for the addPostFrameCallback method from the SchedulerBinding mixin, for the Dart programming language.

However, if you call the function WidgetsBinding.instance.addPostFrameCallbackwithin showCustomMessageBar, duplicate notifications will be displayed each time the widget is rebuilt. This is addPostFrameCallbackbecause it runs after every frame is drawn, so the notification will be triggered again every time the screen updates.

Manage state with StateProvider so that notification is displayed only once

To prevent notifications from appearing twice, you need to use flags that manage the conditions under which notifications are displayed and prevent notifications from appearing again after they have been displayed.

Also, after the user explicitly closes the notification with the “x” icon, the notification message will not be displayed even when transitioning between screens.

Use Riverpod StateProviderto manage whether a notification is dismissed as an app-wide state.

Dart
// ユーザーが通知を閉じたかどうかを追跡するためのプロバイダー
final notificationClosedProvider = StateProvider<bool>((ref) => false);
// 通知が表示されたかどうかを追跡するためのプロバイダー
final notificationShownProvider = StateProvider<bool>((ref) => false);

class HomeTab extends ConsumerStatefulWidget {
  const HomeTab({super.key});
  @override
  _HomeTabState createState() => _HomeTabState();
}

class _HomeTabState extends ConsumerState<HomeTab> {
  @override
  Widget build(BuildContext context) {
    final firmwareUpdate = ref.watch(firmwareUpdateProvider);
    final notificationClosed = ref.watch(notificationClosedProvider);
    final notificationShown = ref.watch(notificationShownProvider);

    // ファームウェア更新があり、まだ通知が閉じられておらず、通知もまだ表示されていない場合に通知を表示
    if (firmwareUpdate.asData?.value.message == "NEW_FIRMWARE_AVAILABLE" &&
        !notificationClosed &&
        !notificationShown) {
      WidgetsBinding.instance.addPostFrameCallback((_) {
        if (!notificationClosed && !notificationShown) {
          showCustomMessageBar(
            context: context,
            message: "新しいファームウェアバージョン(${firmwareUpdate.asData?.value.newFirmwareVersion})がダウンロード可能です。",
            onClose: () {
              ref.read(notificationClosedProvider.notifier).state = true;
            },
            onTapped: () {
              // ダイアログ表示のロジック
            },
          );
          ref.read(notificationShownProvider.notifier).state = true; // 通知が表示されたことを記録
        }
      });
    }

    return Scaffold(
      // UIコンポーネント
    );
  }
}

Operation confirmation

It is called again when the widget is rebuilt, the notification will no longer be redisplayed if it was already displayed or the user dismissed it.

Also, with this modification, we were able to move the message display from the Text widget to the Overlay class, so even if we returned the padding of the Column widget to its original 32px, which originally caused the Overflow error, the error no longer appears.

Dart
return DebugContainer(
  child: Center(
    child: Padding(
      padding: const EdgeInsets.all(32.0),
      child: Column(

Congratulations, congratulations♪

コメント

Copied title and URL