Overlay Widget

Before we jump into the Overlay widget, lets talk about how MaterialApp works.

We have wrapped our entire application within the widget MaterialApp. The MaterialApp widget has a Navigator widget. Navigator widget creates a Overlay widget. So your application is already using an Overlay widget by default which is used by Navigator widget. The Navigator widget uses Overlay to display the routes(pages) on top of each other. That means when you go to a new page, the new page actually floats on top of the previous page. This way the multiple pages actually floats on top of each other. You cannot notice it as they are all forced to be fullscreen. When you pop a page, and go back to previous page, the topmost page from the Overlay widget gets removed.

Overlay widget uses Stack widget to draw the pages. Each children widget of the Overlay widget is called overlay entry and it is represented by the class OverlayEntry. As Overlay widget uses Stack, we can have a positioned item within the stack which will float on top of every pages. This way we can create a snackbar or a notification message that will float on the top of the screen.

With the help of Overlay widget we can add an entry which will float on the page on top of everything else. To insert an overlay entry to Overlay widget, first we need to get the Overlay widget from our current code. Use the following code to get access to the Overlay widget:

Overlay.of(context);

Overlay is an inherited widget, that's why you need to use of method to access the widget. The of method returns the state of the Overlay widget which is OverlayState.

OverlayState overlayState = Overlay.of(context);

OverlayEntry

The overlay state has insert method that takes OverlayEntry as the first parameter which will be used to insert the widget. But first lets create the OverlayEntry widget. The OverlayEntry has builder parameter which should return the positioned widget.

OverlayEntry floatItem = OverlayEntry(
  builder: (context){
    return Positioned(
      top: 100,
      left: 100,
      width: 100,
      height: 100,
      child: Card(
        child: Center(
          child: GestureDetector(
            onTap: (){
              print("Click on Float");
            },
            child: Icon(Icons.favorite, color: Colors.red),
          ),
        ),
      ),
    );
  }
);

Note that the position of the above Positioned element is calculated from top left corner of the screen. Because the Overlay widget covers the entire screen.

Inserting OverlayEntry

Now we can insert the widget into the Overlay widget using the insert method of OverlayState class:

overlayState.insert(floatItem);

When you call the insert method using OverlayState object, it adds the specified OverlayEntry widget into the Overlay widget as the last child and also adds an reference of the Overlay widget to the passed OverlayEntry widget in a private variable _overlay. So every OverlayEntry widget has a reference that points to its container Overlay.

Removing OverlayEntry

To remove the overlay entry from the Overlay widget, call remove method on OverlayEntry widget.

floatItem.remove();

The remove method gets its conainer widget Overlay from the private variable _overlay and removes itself from that container Overlay widget.

Full Example

class Demo extends StatefulWidget{
  @override
  _DemoState createState()=> _DemoState();
}
class _DemoState extends State<Demo>{

  OverlayState overlayState;
  OverlayEntry floatItem;
  bool visible;

  void toggleFloating(){
    if(visible){
      floatItem.remove();
      visible = false;
    }else{
      overlayState.insert(floatItem);
      visible = true;
    }
  }

  @override
  initState(){
    super.initState();

    visible = false;
    overlayState = Overlay.of(context);
    floatItem  = OverlayEntry(
      builder: (context){
        return Positioned(
          top: 100,
          left: 100,
          width: 100,
          height: 100,
          child: Card(
            child: Center(
              child: GestureDetector(
                onTap: (){
                  print("Click on Float");
                },
                child: Icon(Icons.favorite, color: Colors.red),
              ),
            ),
          ),
        );
      }
    );
  }

  @override
  Widget build(BuildContext context){
    return Container(
      child: Center(
        child: GestureDetector(
          onTap: (){
            toggleFloating();
          },
          child: Icon(Icons.favorite, color: Colors.white70),
        ),
      ),
    );
  }
}

The above code will show the overlay card when you tap on the favorite icon. Tapping again on the icon will remove the widget from its parent container Overlay widget.

Use global state to declare OverlayEntry variables

Now, if you show the the overlay card and go back to previous route, the overlay card floatItem that you have added will not be vanished or removed. As we have gone to the previous page, we have lost the variable floatItem, we never can call remove method to remove this from the Overlay widget. This happens because Navigator doesn't track if an overlay entry gets inserted using OverlayState. Navigator only tracks routes that are added via Navigator.push() methods, as Navigator store its routes in its own private variable _history to manage list of all routes. When you call Navigator.push(), the Navigator adds that route in its private list variable _history and then it adds an overlay entry into the Overlay widget. When you call Navigator.pop(), the Navigator object pops the last item from _history and then the poped item also gets removed from Overlay widget. That leaves all overly entry that are added via OverlayState on the Overlay widget. As all the overlay entries are independent from each other Navigator only removes the route that are popped from _history variable.

If you are using Overlay widget for displaying a notification toast, the toast widget should be implemented in a way that it should call the remove method after a specific duration. Becuase you can pass the OverlayEntry variable to the toast widget and that toast widget can call remove method using this variable. You can implement the toast in this way. Generally, it's fine for a toast notification to keep showing between page navigation for a specific duration.

But what if you are implementing a dropdown menu that floats below the widget. In this case you must remove the overlay entry when going to another page or going back to previous page. Otherwise the floated dropdown menu will be displayed on another page. This is awkward. Note that if you remove the overlay entry in dispose method, it will work but not until the transition of the current page finishes.

@override
dispose(){
  if(visible){
    floatItem.remove();
  }
  super.dispose();
}

When you go back to previous page, the current page shows closing animation for a specific duration. When the animation is done then the method dispose gets called. So, the overlay entry will still be visible during that time. Awkward again. What's the solution then? We need to make use of RouteObserver and RouteAware to get around with this problem. Generally you need to call remove method from didPushNext and didPop method.

@override
void didPushNext(){
  print("Push Next");
  toggleFloating();
}

@override
void didPop(){
  print("Pop");
  toggleFloating();
}

To know more go to RouteObserver page.

Non Positioned OverlayEntry

In the above example we have returned a positioned widge from the builder method. What happens if you used a non positioned element? Well, it will force the widget to cover the whole Overlay widget. The following code will not render the SizedBox in specified size, instead the SizedBox will expand to fill its Overflow area.

floatItem  = OverlayEntry(
  builder: (context){
    return SizedBox(
      width: 100,
      height: 100,
      child: DecoratedBox(
        decoration: BoxDecoration(color: Colors.red),
        child: Text("Hey"),
      ),
    );
  }
);

Even though we have specified a value for width and height, the Overflow widget will force its non positioned widget child to fill the whole screen.

Non Positional widget is great when you want to show a modal in which the background of the modal covers the whole screen.

Why OverlayState doesn't provide a mothod to remove the OverlayEntry?

The Overlay widget contains routes that are managed by Navigator widget. If you accidentally remove a route from the Overlay using OverlayState object, that will lead to an unexpected result. That's why the OverlayState didn't provide any method to remove a OverlayEntry. Instead Flutter developer has implemented the remove method on OverlayEntry class, so that a developer can be specific about which overlay entry they want to remove. That's why every OverlayEntry has an _overlay private variable to access the Overlay widget in which they appears. This private variable is the reason for why an OverlayEntry can be at most in one Overlay widget at a time.