How to display something on top of any screen/page in Flutter?

Difficulty: Intermediate

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
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import 'package:flutter/material.dart';
import 'dart:async';

class ShowNotificationIcon {

    void show(BuildContext context) async {
        OverlayState overlayState = Overlay.of(context);
        OverlayEntry overlayEntry = new OverlayEntry(builder: _build);

        overlayState.insert(overlayEntry);

        await new Future.delayed(const Duration(seconds: 2));

        overlayEntry.remove();
    }

    Widget _build(BuildContext context){
      return new Positioned(
        top: 50.0,
        left: 50.0,
        child: new Material(
            color: Colors.transparent,
            child: new Icon(Icons.warning, color: Colors.purple),
        ),
      );
    }
}

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;

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

        await new Future.delayed(duration);

        overlayEntry.remove();

        _isVisible = false;
    }
}

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

For further explanation on the notion of Animation, please refer to my article on this topic.

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

    final Widget widget;
    final Offset position;

    @override
    _BlinkingToastWidgetState createState() => new _BlinkingToastWidgetState();
}

class _BlinkingToastWidgetState extends State<BlinkingToastWidget>
    with SingleTickerProviderStateMixin {

AnimationController _controller;
  Animation<double> _animation;

  @override
  void initState() {
    super.initState();
    _controller = new AnimationController(
        duration: const Duration(milliseconds: 500), vsync: this);
    _animation = new Tween(begin: 0.0, end: 1.0).animate(new CurvedAnimation(
      parent: _controller,
      curve: new 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;
  }

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

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

To invoke this BlinkingToast:

BlinkingToast toast = new BlinkingToast();

toast.show(
    context: context,
    externalBuilder: (BuildContext context){
        return new Icon(Icons.warning, color: Colors.purple);
    },
    duration: new Duration(seconds: 5),
    position: new 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.