How to create a Toast or Notifications? Notion of Overlay

Compatibility
Last Reviewed
Mar 30, 2023
Published on
Jun 26, 2018
Flutter
v 3.13.x
Dart
v 3.1.x

Forewords

Lately I was writing some code to work with WebSockets and I needed to display an icon on top any screen/page to inform the user when a notification was sent by the Server.

I tried with PopupRoute, showDialog... but could never obtain what I wanted to achieve.

When we use Routes, the whole screen is overlayed and the user looses the ability to continue working "as normal" with the current page since the latter is replaced (overlayed) by another one. Therefore, I continued with my investigations and found the notion of Overlay and OverlayEntry.

By reading the source code of Flutter, I discovered that Flutter uses an OverlayEntry to show the drag avatar (see Draggable). Therefore, I understood that this was what I was looking for.

Overlay

The Flutter documentation says: "An overlay is a Stack of entries that can be managed independently. Overlays let independent child widgets 'float' visual elements on top of other widgets...". Well.. not very self explanatory.

With my own very simple words.

An overlay is (at first glance), nothing else but a layer (StatefulWidget) which is on top of everything, which contains a Stack to which we can append any Widget.

These Widgets are called OverlayEntry

OverlayState

An Overlay is a StatefulWidget. The OverlayState is the State of a particular instance of the Overlay widget, responsible for the rendering.

OverlayEntry

An OverlayEntry is a Widget that we insert in an Overlay. An OverlayEntry may only be in at most one overlay at a time.

Because an Overlay uses a Stack layout, overlay entries can use Positioned and AnimatedPositioned to position themselves within the overlay.

Well, this is exactly what I need: to be able to display my notification icon anywhere on the screen.

Let's directly jump into some code

The following class displays an Icon at coordinates (50.0, 50.0) and removes it after 2 seconds.


1
2import 'package:flutter/material.dart';
3import 'dart:async';
4
5class ShowNotificationIcon {
6
7    void show(BuildContext context) async {
8        final OverlayState overlayState = Overlay.of(context);
9        final OverlayEntry overlayEntry = OverlayEntry(builder: _build);
10
11        overlayState.insert(overlayEntry);
12
13        await Future.delayed(const Duration(seconds: 2));
14
15        overlayEntry.remove();
16    }
17
18    Widget _build(BuildContext context){
19      return Positioned(
20        top: 50.0,
21        left: 50.0,
22        child: Material(
23            color: Colors.transparent,
24            child: Icon(Icons.warning, color: Colors.purple),
25        ),
26      );
27    }
28}
29

As you can see, in order to be able to show the icon, we need to provide a context. Why?

The official documentation does not explain this but, having a look at the source code, we see that an Overlay is created for each Route and hence belongs to a context tree (see my article on the notion of context). Therefore it is necessary to find the OverlayState that corresponds to a certain context (Line #7).

This is it! From this example, we may derive any type of overlay content and behaviour.

In order to illustrate this, let's build some kind of blinking Toast that displays a Widget at a certain position on the screen and which disappears after a certain duration.

Example: Blinking Toast

This first class is a generalization of the previous example. It allows the provision of an external Widget constructor.



import 'dart:async';
import 'package:flutter/material.dart';

class BlinkingToast {
    bool _isVisible = false;

    ///
    /// BuildContext context: the context from which we need to retrieve the Overlay
    /// WidgetBuilder externalBuilder: (compulsory) external routine that builds the Widget to be displayed
    /// Duration duration: (optional) duration after which the Widget will be removed
    /// Offset position: (optional) position where you want to show the Widget
    ///
    void show({
        required BuildContext context,
        required WidgetBuilder externalBuilder, 
        Duration duration = const Duration(seconds: 2),
        Offset position = Offset.zero,
        }) async {

        // Prevent from showing multiple Widgets at the same time
        if (_isVisible){
            return;
        }

        _isVisible = true;

        final OverlayState overlayState = Overlay.of(context);
        final OverlayEntry overlayEntry = OverlayEntry(
            builder: (BuildContext context) => BlinkingToastWidget(
                widget: externalBuilder(context),
                position: position,
            ),
        );
        overlayState.insert(overlayEntry);

        await Future.delayed(duration);

        overlayEntry.remove();

        _isVisible = false;
    }
}

This second class displays the Widget at a certain position on the Screen and makes it blink.



class BlinkingToastWidget extends StatefulWidget {
  const BlinkingToastWidget({
      super.key,
      required this.widget,
      required this.position,
  });

  final Widget widget;
  final Offset position;

  
  State<BlinkingToastWidget> createState() => new _BlinkingToastWidgetState();
}

class _BlinkingToastWidgetState extends State<BlinkingToastWidget>
  with SingleTickerProviderStateMixin {

  late AnimationController _controller;
  late Animation<double> _animation;

  
  void initState() {
    super.initState();
    _controller = AnimationController(
        duration: const Duration(milliseconds: 500), vsync: this);
    _animation = Tween(begin: 0.0, end: 1.0).animate(CurvedAnimation(
      parent: _controller,
      curve: Interval(0.0, 0.5)
    ))
      ..addListener(() {
        if (mounted){
          setState(() {
            // Refresh
          });
        }
      })
      ..addStatusListener((AnimationStatus status){
        if (status == AnimationStatus.completed){
          _controller.reverse().orCancel;
        } else if (status == AnimationStatus.dismissed){
          _controller.forward().orCancel;
        }
      });
    _controller.forward().orCancel;
  }

  
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  
  Widget build(BuildContext context) {
    return Positioned(
        top:  widget.position.dy,
        left: widget.position.dx,
        child: IgnorePointer(
          child: Material(
            color: Colors.transparent,
            child: Opacity(
              opacity: _animation.value,
              child: widget.widget,
            ),
          ),
        ),
      );
    }
  }
}

To invoke this BlinkingToast:



BlinkingToast toast = BlinkingToast();

toast.show(
    context: context,
    externalBuilder: (BuildContext context){
        return Icon(Icons.warning, color: Colors.purple);
    },
    duration: Duration(seconds: 5),
    position: Offset(50.0, 50.0),
);

Conclusion

This is a very short article, only aimed at sharing the way to display a Widget on top of any screen.

I hope you found this article useful.

Stay tuned for the next articles and happy coding.

0 Comments
Be the first to give a comment...
© 2024 - Flutteris
email: info@flutteris.com

Flutteris



Where excellence meets innovation
"your satisfaction is our priority"

© 2024 - Flutteris
email: info@flutteris.com