Overlays in Flutter: Getting Started

Learn Flutter’s own way to display overlays like popUps, modals and dialog boxes with the help of popUpRoutes. By Michael Malak.

4 (1) · 1 Review

Download materials
Save for later
Share

Learn Flutter’s own way to display overlays like popUps, modals and dialog boxes with the help of popUpRoutes.

As a developer, you’re always looking for ways to build visually appealing apps that provide an excellent experience for your users. At some point, you’ll have to work with overlays in Flutter that display on top of your widget tree, like modals, dialogs, or popups. In this article, you’ll learn how to show overlays in Flutter.

Flutter has different types of routes; you already use PageRoutes to transition between pages. In this tutorial, you’ll learn more about PopupRoutes and how to display overlays in flutter, like dialogs and modals.

You’ll learn more about ready-to-use public functions that Flutter provides to show dialogs and menus. These functions show route-aware overlays, so you dismiss them by pressing the system back button. However, you’ll also learn how to create your own custom way of showing your overlays when you want to have more control.

By the end of this tutorial, you’ll learn:

  • What overlays are and how they work.
  • When to use different types of overlays in Flutter.
  • How to work with different types of overlays.
Note: This tutorial assumes that you have some experience with Flutter and Flutter widgets. If you don’t, check out our Getting Started with Flutter tutorial, our Flutter UI Widgets video course or our Flutter Apprentice book.

Getting Started

Download the starter project by clicking the Download Materials button at the top or bottom of the tutorial.

You’ll work on the Simply Noted app, a single-page app that displays a list of notes. Here’s what you’ll be able to do using overlays in flutter:

  • Sort notes: View a popup to sort notes by their date of creation.
  • Create and modify a note: Open an overlay to create or modify a note.
  • Confirm deleting a note: View a confirmation dialog before deleting a note.
  • Confirm editing a note: Show a confirmation overlay to confirm editing a note.

Here’s what the app will look like when you’re done:

Showcase the final Simply Noted app

Now, it’s time to take a look at the starter project.

Setting up the Starter Project

The starter project already contains the logic to save, edit the notes in the cache and retrieve them from the cache.

This tutorial uses Android Studio 2021.1.1. Some of the screenshots are specific to it, but you can also follow along with Visual Studio Code or IntelliJ IDEA.

Open Android Studio and select Open an Existing Project. Then, select the starter folder from the downloaded materials.

Open Project in Android Studio

Open pubspec.yaml and click the Pub get tab that appears in your IDE, or run the command flutter pub get in the terminal to get the packages:

Fetch dependencies declared in pubspec.yaml file

For this tutorial, the most important files in the project are:

  1. lib/ui/notes/widgets/sort_notes_icon_widget.dart: The IconButton widget that displays the sort icon.
  2. lib/ui/notes/widgets/delete_note_icon_widget.dart: The IconButton widget that displays the delete icon.
  3. lib/service/router_service/router_service.dart: The service responsible for routing and generating MaterialPageRoutes.
  4. lib/ui/notes/widgets/note_item_widget.dart: The widget class representing a note in the note list.

Now open lib/main.dart. Then build and run to see the app on your target emulator or device. The app launches with a screen of populated notes:

Note List Starter

Now that you know what the starter project contains, you’ll take a deeper look at what overlays are and how you’ll display overlays in Flutter.

Understanding Overlays

Overlays, simply put, are floating widgets on top of other widget children in your app. Flutter has different ways of showing overlays depending on your needs.

The base class for all the routes in Flutter is Route. It’s an abstract class that defines an interface for the Navigator. Routes define methods for handling state change, changes in the stack, and callbacks for when the route is popped.

For Dialogs and Modals, it’s better to keep them consistent and aware of the route. Flutter will then allow you to push an implementation for an abstract PopupRoute to the Navigator using global accessible methods like showDialog, showMenu and showModalBottomSheet.

PopupRoute relation with Flutter methods

Viewing the Pop-up Menu to Sort Notes

Instead of having two icons on the app bar for sorting the notes, you want to have one sort icon that displays a popup menu. Flutter provides you with PopupMenuButton; which is a widget that uses showMenu under the hood.

PopupRoute relation with PopupMenuButton

Start by going to lib/ui/notes/widgets/sort_notes_icon_widget.dart and add the following import at the top of the file:

import '../../../data/models/sort_type_model.dart';

Now, you can replace the body of the build method with:

// 1
return PopupMenuButton<SortType>(
  
  // 2
  icon: const Icon(Icons.sort),
  
  // 3
  onSelected: (sortType) {

    // 4
    switch (sortType) {
      case SortType.oldestFirst:
        sortNotesByOldestFirst();
        break;
      case SortType.newestFirst:
        sortNotesByNewestFirst();
        break;
    }
  },

  // 5
  itemBuilder: (context) => [
    const PopupMenuItem(
      child: Text('Newest first'),
      value: SortType.newestFirst,
    ),
    const PopupMenuItem(
      child: Text('Oldest first'),
      value: SortType.oldestFirst,
    ),
  ],
);

Here’s the breakdown of the code:

  1. The build method returns the PopupMenuButtonwidget which has a type of SortType. SortType is an enum specifying different types of sorting present in the file that you imported at the top.
  2. You provide a sort icon for the PopupMenuButton to display.
  3. onSelected is a callback function that you pass, and is triggered when you click the button. It returns an object of type SortType.
  4. Implement a switch case for the onSelected callback which takes a sortType as a parameter. In case of having a SortType.oldestFirst, you sort the notes by the oldest, and in case of having a SortType.newestFirst, you sort by the newest.
  5. A builder that builds the items inside of the popup menu. You display a list of two PopupMenuItems, and provide each with a different value of SortType.

Build and run; you’ll find that the two sort icons on the app bar have been replaced with the PopupMenuButton.

Todo 1 result

Now you know how to use PopupMenuButton. Next, you’ll dig a bit deeper and use showDialog directly.

Showing Delete Confirmation Dialog

When deleting a note, you want to show a confirmation dialog to make sure the user wants to delete the note. To do that, head to lib/ui/notes/widgets/delete_note_icon_widget.dart. Add the following code after the build method to DeleteNoteIconWidget:

Widget buildConfirmationDialog(BuildContext context) {
  // 1
  final cancelIcon = IconButton(
    icon: const Icon(Icons.close),
    onPressed: () => Navigator.pop(context),
  );

  // 2
  final confirmDeleteIcon = IconButton(
    icon: const Icon(Icons.delete_rounded),
    onPressed: () {
      onDelete();
      Navigator.pop(context);
    },
  );

  // 3
  return AlertDialog(
    title: const Text('Warning'),
    content: const Text('Are you sure you want to delete this note?'),
    actions: [cancelIcon, confirmDeleteIcon],
  );
}

Here you:

  1. Create a cancel IconButton that pops the dialog when tapped. Since the dialog will be on top of the navigation stack, closing it is possible using Navigator.pop
  2. Implement confirm deletion IconButton. When you tap this IconButton, you call onDelete() function which deletes the selected note and then pop’s the dialog.
  3. Return AlertDialog with both IconButtons, cancelIcon and confirmDeleteIcon as list of actions.

Now, replace your build method with the following code:

// 1
return IconButton(
  icon: const Icon(Icons.delete),
  // 2
  onPressed: () => showDialog(
    context: context,
    builder: buildConfirmationDialog,
  ),
);

Here’s what’s happening above:

  1. You return an IconButton that displays a delete icon.
  2. When you tap the button, it triggers showDialog and builds the previously created AlertDialog in buildConfirmationDialog widget.

Build and run the app to check that when you tap on the delete icon, and you get a confirmation dialog.

Todo 2 result

Congratulations! You’ve successfully built an overlay in Flutter: you deserve a pat on the back.

Now that you understand how to use basic methods for displaying overlays in Flutter, let’s dive deeper to understand PopupRoute.

Adding a Note as an Overlay

When you select add icon in the app bar, you want to push SaveNotePage on top of the displayed list of notes as an overlay. Since you want to control the overlay and add transition animations, you’ll create a custom route instead of relying on Flutter’s MaterialPageRoute.

Creating a Custom PopupRoute

In lib/service/router_service/router_service.dart, append the following class at the end of the file:

// 1
class CustomPopupRoute extends PopupRoute {
  // 2
  CustomPopupRoute({
    required this.builder,
    RouteSettings? settings,
  }) : super(settings: settings);

  final WidgetBuilder builder;
  
  // 3
  @override
  Color get barrierColor => Colors.black54.withAlpha(100);
  @override
  bool get barrierDismissible => true;
  @override
  String get barrierLabel => 'customPopupRoute';

  // 4
  @override
  Duration get transitionDuration => const Duration(milliseconds: 300);

  // 5
  @override
  Widget buildPage(
    BuildContext context,
    Animation<double> animation,
    Animation<double> secondaryAnimation,
  ) => builder(context);

  // 6
  @override
  Widget buildTransitions(
    BuildContext context,
    Animation<double> animation,
    Animation<double> secondaryAnimation,
    Widget child,
  ) {
    return ScaleTransition(
      scale: animation,
      child: FadeTransition(
        opacity: animation,
        child: child,
      ),
    );
  }
}

Here’s a detailed breakdown of the code above:

  1. You created your custom route that extends PopupRoute.
  2. By passing settings, a RouteSettings object to super in the constructor, you pass it to the base PopupRoute class. This ties the custom route to a page in case the settings is a subclass of Page. A page-based route, as opposed to a pageless route, is created from Page.createRoute during Navigator.pages updates.
  3. When defining a route, you implement barrier-specific overrides. barrierColor is the color of the barrier between the route and the previous one. In this case, you made it slightly transparent. barrierDismissible is a Boolean responsible if the route can be dismissed, by clicking outside of its defined bounded box. barrierLabel is the semantic label used for a dismissible barrier.
  4. The transitionDuration is the animation duration of opening the custom route.
  5. Override the buildPage to return the passed builder with the newly created context. This builder contains the widget that you want to add as an overlay.
  6. Specify ScaleTransition and FadeTransition as the animation transitions when opening and closing the custom route.

Now that you created a CustomPopupRoute, you’re ready to use it with SaveNotePage screen.

Using Custom PopupRoute when Adding a Note

While you’re at lib/service/router_service/router_service.dart, replace the implementation at # TODO 3: Create then use CustomPopupRoute with:

case SaveNotePage.route:
  // 1
  return CustomPopupRoute(
    // 2
    builder: (_) => SaveNotePage(
      onNoteSaved: settings.arguments as NoteCallback,
    ),
    // 3
    settings: settings,
  );

Here’s what you did:

  1. Switched the return to CustomPopupRoute instead of MaterialPageRoute. Now you’ll be using your CustomPopupRoute that you created before.
  2. Returned SaveNotePage for the builder property of the route and passed onNoteSaved as the callback with type NoteCallBack from the RouteSettings arguments.
  3. CustomPopupRoute takes a RouteSetting parameter as settings. Provide the settings value as the RouteSettings property to the CustomPopupRoute.

Build and run; it functions as you’d expect when you click the add icon.

Todo 3 Result

Understanding Overlay Widgets

As previously mentioned, it’s most common to use overlays created by the Navigator. The Navigator will manage the visual appearance of its routes, including PopupRoutes, if they’re an overlay. However, Flutter allows you to create an Overlay directly.

Overlay widget is a StatefulWidget that stacks independent child widgets and allows them to float on top of your widget tree. It manages each of its OverlayEntry children very similarly to how the Stack widget works. You use OverlayState to insert OverlayEntrys into the Overlay widget using the insert and insertAll functions.

Note: Overlay‘s main use case is related to being able to insert widgets on top of the pages in an app, as opposed to Stack, that simply displays a stack of widgets.

Opening Note Details Overlay

You want to edit a note as an overlay by tapping on it without pushing the SaveNotePage to the navigation stack. Instead, you’ll use the Overlay widget. First, you’ll create a generic implementation to manage Overlay widget. Then, you’ll use this implementation to display note details as an overlay.

Creating Overlay mixin

You’ll create a shared mixin that will add and remove overlays in Flutter for a cleaner approach.

Note: Mixins are a way to reuse methods or variables among otherwise unrelated classes. To learn more you can check out the Dart Apprentice – Chapter 9: Advanced Classes.

Create a new dart file in lib/ui/_shared/mixin/ called overlay_mixin.dart, and add the following code to it:

import 'package:flutter/material.dart';
import '../utils/app_colors.dart';

// 1
mixin OverlayStateMixin<T extends StatefulWidget> on State<T> {
  // 2
  OverlayEntry? _overlayEntry;

  // 3
  void removeOverlay() {
    _overlayEntry?.remove();
    _overlayEntry = null;
  }

  // 4
  Widget _dismissibleOverlay(Widget child) => Stack(
        children: [
          Positioned.fill(
            child: ColoredBox(
              color: AppColors.barrierColor,
              child: GestureDetector(
                onTap: removeOverlay,
              ),
            ),
          ),
          child,
        ],
      );

  // 5
  void _insertOverlay(Widget child) {

   // 6
   _overlayEntry = OverlayEntry(
      builder: (_) => _dismissibleOverlay(child),
    );

    // 7
    Overlay.of(context)?.insert(_overlayEntry!);
  }

}

This is what the code does:

  1. Creates a new mixin named OverlaysStateMixin that you’ll use with the State class of StatefulWidgets.
  2. Adds a single private nullable variable OverlayEntry. You’ll use this variable to manage the overlay entries. In this mixin, you’ll only manage one overlay.
  3. A void function named removeOverlay that removes the overlayEntry by calling _overlayEntry?.remove() function assigned in the OverlayEntry before setting it to null.
  4. _dismissibleOverlay is a private function that allows for calling removeOverlay() when clicking outside the child boundaries.
  5. A private function you use to display a Widget as an overlay.
  6. You assign OverlayEntry with the dismissable child.
  7. Overlay.of(context) yields OverlayState object which you use to insert your newly assigned OverlayEntry object.

Now that you can insert and remove an overlay using the mixin, you want to expose a public function to toggle it. Add the following to OverlayStateMixin:

// 1
bool get isOverlayShown => _overlayEntry != null;

// 2
void toggleOverlay(Widget child) =>
    isOverlayShown ? removeOverlay() : _insertOverlay(child);

Here you:

  1. Implement a getter method to check if the OverlayEntry is visible by checking if it was not null.
  2. Expose a public function to toggle viewing the single _overlayEntry.

Since you’ll use OverlayStateMixin with StatefulWidgets, you can override some methods to remove the overlay in certain senarios. You add the following to OverlayStateMixin:

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

@override
void didChangeDependencies() {
  removeOverlay();
  super.didChangeDependencies();
}

You override state functions dispose and didChangeDependencies to remove the displayed overlay when they trigger.

Now that you created a shared mixin to manage a single OverlayEntry, you’ll use it to display an overlay that allows editing a note item.

Using Overlay Mixin to Edit a Note

You want to show SaveNotePage widget as an overlay when you tap on a note item. This page will let you edit and save the note.

To achieve this, first go to lib/ui/notes/widgets/note_item_widget.dart, and at the top of the page add an import of your newly created overlay mixin:

import '../../_shared/mixin/overlay_mixin.dart';

Now, you can use the mixin with the _NoteItemWidgetState as the following:

class _NoteItemWidgetState extends State<NoteItemWidget>
    with OverlayStateMixin { 
       ... 
    }

Here, you are extending your _NoteItemWidgetState class with state of NoteItemWidget and adding the OverlayStateMixin with a with keyword.

Replace the code at /// TODO 4: Create and use OverlayMixin to show SaveNotePage as an overlay with:

void onNoteTap() {
  // 1
  toggleOverlay(
    SaveNotePage(
      
      // 2
      noteToEdit: note,

      // 3
      onNoteSaved: (editedNote) {
        widget.onEdit(editedNote);
        removeOverlay();
      },
    ),
  );
}

Here’s the breakdown of the code:

  1. In the onNoteTap() function you call the toggleOverlay to display SaveNotePage as an overlay.
  2. You pass the displayed note as an argument to noteToEdit.
  3. You implement callback function when you tap the save button which takes the editedNote as a parameter. Call the widget.onEdit function which will save the editedNote that you’ve passed as a parameter. This will save the notes and then remove the overlay by calling removeOverlay function

Build and run. When you click a note item in the list, you’ll see the edit note overlay.

Todo 4 result

The edit overlay works as expected, but instead of closing the overlay when you tap your device’s back button, the app closes. You’ll solve this next.

App closes on native back press

Removing the Overlay on Native Back Press

Since you’re not using the Navigator to manage your overlay, the edit note overlay is not closed when you tap back on your device, even though you expect the modal to be closed.

While you’re in lib/ui/notes/widgets/note_item_widget.dart, replace the build function at /// TODO 5: Use [WillPopScope] and [isOverlayShown] to prevent popping the page when used with the Overlay with:

// 1
@override
Widget build(BuildContext context) => WillPopScope(
      onWillPop: () async {
        // 2
        if (isOverlayShown) {
          removeOverlay();
          return false;
        }
        return true;
      },
      child: buildListTile(),
    );

Here’s what you did:

  1. Wrap buildListTile() with WillPopScope. The WillPopScope is triggered when a pop action is taking place. This has a method onWillPop which pop’s a widget if the return is true and prevents the pop if the return is false.
  2. Implement onWillPop callback that vetos attempts to close the Route. When isOverlayShown is true, you remove the overlay by removeOverlay function and prevent closing the route by returning a false value, otherwise, you allow closing it.

Build and run. Now, when you click back, the edit note overlay should close.

Todo 5 result

Did you know: Draggable uses Overlay widget under the hood.

Confirming Before Editing Notes

Now that you have already viewed an overlay to edit a note item, you want to show another overlay on top of it to confirm editing before saving it to the cache.

Head to lib/ui/save_note/save_note_page.dart, and add the following imports at the top of the file:

import '../_shared/mixin/overlay_mixin.dart';
import 'widgets/save_note_confirmation_widget.dart';

You can now add OverlayStateMixin to _SaveNotePageState as follows:

class _SaveNotePageState extends State<SaveNotePage> with OverlayStateMixin {

Add the following getters to the body of code>_SaveNotePageState:

// 1
bool get hasChanges {

  // 2
  final isHeaderChanged = widget.noteToEdit?.header != note.header;
  final isTextChanged = widget.noteToEdit?.text != note.text;
  return isHeaderChanged || isTextChanged;
}

// 3
bool get isEditMode => widget.noteToEdit != null;
  1. Add a getter to get a bool if the note has been edited.
  2. Detect if either the header or the text body has been modified.
  3. If widget.noteToEdit is passed to the widget, then it’s in edit mode.

Replace the code beneath /// TODO 6: Check page has edits and show confirmation overlay before saving the edits with:

// 1
if (isEditMode && hasChanges) {
  toggleOverlay(
    SaveNoteConfirmationWidget(
      // 2
      onConfirm: () => widget.onNoteSaved(note),
      onCancel: removeOverlay,
    ),
  );
} else {
  // 3
  widget.onNoteSaved(note);
}

Here’s what’s happening above:

  1. In the conditionals, if the note is in edit mode and has been modified, you show an overlay displaying SaveNoteConfirmationWidget. This has two buttons; one to confirm the save and other to cancel the save.
  2. When you tap confirm button, you save the note by calling the onNoteSaved callback function and pass the edited note as a parameter. When you tap cancel, you remove the overlay by calling the removeOverlay function.
  3. In case you’re not in edit mode or the note has changes, you trigger onNoteSaved callback, the else part of the conditionals.

Build and run. When you edit a note and modify the text, you get a confirmation overlay before you save the edits.

Todo 6 result

Congratulations again, you have successfully created overlays in Flutter that you can use accordingly.

Where to Go From Here?

Download the completed project files by clicking the Download Materials button at the top or bottom of the tutorial.

You now have a deeper understanding of overlays in Flutter and, more importantly, when and how to use them. When you find the need, you can create your own PopupRoute or implement your own way of handling the Overlay widget.

Check out the following links to learn more about some of the concepts in this tutorial:

We hope you enjoyed this tutorial. If you have any questions or comments, please join the forum discussion below!