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);
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.
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
.
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.
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.
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.
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.
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.