Displaying a loading overlay or progress HUD in Flutter
Tags: flutter, dart, loading, overlay
Published: 2022-05-19 18:13:53

## Introduction
Sometimes, in an app, you want to perform an asynchronous operation and want to prevent the user from tapping/using the app while this operation is in progress. A simple example would be when you create a new item/to-do in your to-do list app. After tapping on "save" or an equivalent button, the save operation would very likely be asynchronous; it might involve storing data to local storage, performing a network request, or both. This is usually a very quick operation, taking less than a second. But sometimes it could take a bit longer. During that time, you don't want users accidentally tapping the save button twice, or changing inputs.

The simplest solution to this would be to display a loading overlay while the operation is in progress. To do this, we're going to need a widget! And in this tutorial, we will do just that.

## Defining a loading overlay, or progress HUD
To make sure we're on the same page, this is what the end result will be like:

![Loading overlay](https://cdn.hashnode.com/res/hashnode/image/upload/v1647459020650/cYyC68mLh.png)

Just a circular progress indicator with a semi-opaque dark background, covering the current screen and preventing any user input. This is typically called an overlay or a heads-up-display (HUD).

## Re-inventing the wheel
Before we get started, you might not be interested in writing a custom solution for this and would rather use a package for this. If this is the case, there are tens of packages to do just this. Just search for "loading overlay", "loader overlay", or "progress hud" on [pub.dev](https://pub.dev). I haven't used any personally, but [loader_overlay](https://pub.dev/packages/loader_overlay) looks well maintained and has a nice API.

At the same time, this is almost trivial to implement, so I'd encourage you to just use your own solution (or copy this one!), and simply adjust it to work for you. Introducing an entirely new package to only do this might be a bit too much.

## Building the overlay in 2 steps
In this tutorial, we will build the loading overlay widget in 3 steps, building on top of the example Flutter counter app. In the first step, we'll simply make the overlay work on the main screen of the app, without any re-usable code. In the second step, we'll extract our overlay code to a new widget that we can re-use anywhere.

If you're just here for the code, just go ahead and [skip to step #2](#heading-step-2-extracting-the-loadingoverlay-widget), or see the code in the repository [here](https://github.com/dartling/loading_overlay).

### Step #1: Using a `Stack`
In this tutorial we'll be working with the example Flutter app. If you want to follow along, you can create this with the following command:

```sh
flutter create <app_name>
```

The loading overlay widget is actually very simple; it is just the current screen/view/widget, wrapped in a `Stack`, with the semi-opaque overlay and a centered circular progress indicator at the top of the stack.

```dart
@override
Widget build(BuildContext context) {
  return Stack(
    children: [
      Scaffold(
        ...
      ),
      const Opacity(
        opacity: 0.8,
        child: ModalBarrier(dismissible: false, color: Colors.black),
      ),
      const Center(
        child: CircularProgressIndicator(),
      ),
    ],
  );
}
```

With the change above, we now have a permanent loading overlay in front of the `Scaffold`. Let's introduce a new state variable, `_isLoading`, and only show the `Opacity` and `Center` widgets if its value is `true`.

```dart
class _MyHomePageState extends State<MyHomePage> {
  int _counter = 0;
  bool _isLoading = false;

  @override
  Widget build(BuildContext context) {
    return Stack(
      children: [
        Scaffold(
          ...
        ),
        if (_isLoading)
          const Opacity(
            opacity: 0.8,
            child: ModalBarrier(dismissible: false, color: Colors.black),
          ),
        if (_isLoading)
          const Center(
            child: CircularProgressIndicator(),
          ),
      ],
    );
  }
}
```

Let's make a small tweak to the `_incrementCounter` method to test this. When the method is called, we set `_isLoading` to `true`, wait 3 seconds, and then set it back to `false` and increment the counter.

```dart
void _incrementCounter() async {
  setState(() {
    _isLoading = true;
  });
  await Future.delayed(const Duration(seconds: 3));
  setState(() {
    _counter++;
    _isLoading = false;
  });
}
```

And that's it! Easy, right? Now, this loading overlay code is mixed up with the other view logic, and if we have to push a new view on top of this one (`Navigator.push(context, ...)`), we'd need to implement this all over again.

### Step #2: Extracting the `LoadingOverlay` widget
```dart
// loading_overlay.dart
class LoadingOverlay extends StatefulWidget {
  const LoadingOverlay({Key? key, required this.child}) : super(key: key);

  final Widget child;

  static _LoadingOverlayState of(BuildContext context) {
    return context.findAncestorStateOfType<_LoadingOverlayState>()!;
  }

  @override
  State<LoadingOverlay> createState() => _LoadingOverlayState();
}

class _LoadingOverlayState extends State<LoadingOverlay> {
  bool _isLoading = false;

  void show() {
    setState(() {
      _isLoading = true;
    });
  }

  void hide() {
    setState(() {
      _isLoading = false;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Stack(
      children: [
        widget.child,
        if (_isLoading)
          const Opacity(
            opacity: 0.8,
            child: ModalBarrier(dismissible: false, color: Colors.black),
          ),
        if (_isLoading)
          const Center(
            child: CircularProgressIndicator(),
          ),
      ],
    );
  }
}
```

Notice the `LoadingOverlay.of()` function, which actually returns `_LoadingOverlayState`. In the state we expose two public functions, `show` and `hide`, which respectively show and hide the loading overlay.

The `context.findAncestorStateOfType<T>()` function returns any ancestor matching this state class, which can be `null` if there is no such ancestor. In this example, we can simply assume there will always be one and use `!`. Or, we could assert it is not `null` before returning it, throwing an error with a useful message.

Now, in order for `_LoadingOverlayState` to be an "ancestor" in our app's widget tree, we need to wrap our main view with the `LoadingOverlay` widget, by making the following changes:

```dart
// MyApp
@override
Widget build(BuildContext context) {
  return MaterialApp(
    title: 'Flutter Demo',
    theme: ThemeData(
      primarySwatch: Colors.blue,
    ),
    home: const LoadingOverlay(
      child: MyHomePage(title: 'Loading Overlay'),
    ),
  );
}
```

Note that we reverted the changes initially made in `_MyHomePageState`, as the `Stack` and overlay widgets have now been moved to the `LoadingOverlay` widget.

By simply passing `MyHomePage` as a child in `LoadingOverlay`, we can toggle the loading overlay at any point within the `MyHomePage` widget or any of its child widgets by calling `LoadingOverlay.of(context)`.

To see our changes in action, let's change the `_incrementCounter` method again.

```dart
void _incrementCounter() async {
  LoadingOverlay.of(context).show();
  await Future.delayed(const Duration(seconds: 3));
  setState(() {
    _counter++;
  });
  LoadingOverlay.of(context).hide();
}
```

We now have a reusable `LoadingOverlay` widget that we can use any time we want to toggle such an overlay. No external packages needed, and all in a stateless widget of less than 50 lines of code.

Note that if we push a new view/widget on top of this one, the context there will not have a `LoadingOverlay` ancestor. So we'd need to wrap any newly pushed views with another `LoadingOverlay`.

```dart
Navigator.push(
    context,
    MaterialPageRoute(
        builder: (_) => const LoadingOverlay(child: NewPage())));
```

And that's it for the main part of this tutorial. Next, we will make some small improvements to the `LoadingOverlay` widget and make it more customizable. After that, in the following step, we will make the widget stateless rather than stateful. If you're not interested, feel free to skip ahead to the [end of the post](#heading-wrapping-up).

## Improvement #1: Tweaks
In some cases, the operations you show the loading overlay for might take less than a second, or even half a second. In such cases, you might still want to show an overlay to block user interactions, but not the spinner/circular progress indicator.

We can make a small tweak to our `LoadingOverlay` widget to delay the circular progress indicator by a duration of our choice, by using a `FutureBuilder`.

```dart
class LoadingOverlay extends StatefulWidget {
  const LoadingOverlay({
    Key? key,
    required this.child,
    this.delay = const Duration(milliseconds: 500),
  }) : super(key: key);

  final Widget child;
  final Duration delay;

  ...
}
```

```dart
// _LoadingOverlayState
...
Center(
  child: FutureBuilder(
    future: Future.delayed(widget.delay),
    builder: (context, snapshot) {
      return snapshot.connectionState == ConnectionState.done
          ? const CircularProgressIndicator()
          : const SizedBox();
    },
  ),
),
...
```

The `delay` parameter is optional and defaults to 500 milliseconds.

Another change we can do is blur the background when we show the overlay. This can be done, of course, with another widget; `BackdropFilter` and `ImageFilter.blur`.

```dart
BackdropFilter(
  filter: ImageFilter.blur(sigmaX: 4.0, sigmaY: 4.0),
  child: const Opacity(
    opacity: 0.8,
    child: ModalBarrier(dismissible: false, color: Colors.black),
  ),
),
```

![Blurred overlay](https://cdn.hashnode.com/res/hashnode/image/upload/v1647463062106/RU-kT26gv.png)

Another potential tweak would be to show something else rather than a `CircularProgressIndicator`. If you want this to vary depending on what you use it for, it could just be an additional `Widget` parameter that you can simply pass when you create a `LoadingOverlay`. Or it could even be optional and default to `CircularProgressIndicator`.

## Improvement #2: Going stateless
I'm a big fan of stateless widgets, and try to avoid stateful widgets when I can. For this loading overlay widget, the only state we need to manage and change is a boolean value that determines whether the overlay is shown or not. Rather than with a stateful widget, we could achieve this by using a `ValueNotifier` and a `ValueListenableBuilder`.

```dart
class LoadingOverlayAlt extends StatelessWidget {
  LoadingOverlayAlt({Key? key, required this.child})
      : _isLoadingNotifier = ValueNotifier(false),
        super(key: key);

  final ValueNotifier<bool> _isLoadingNotifier;
  final Widget child;

  static LoadingOverlayAlt of(BuildContext context) {
    return context.findAncestorWidgetOfExactType<LoadingOverlayAlt>()!;
  }

  void show() {
    _isLoadingNotifier.value = true;
  }

  void hide() {
    _isLoadingNotifier.value = false;
  }

  @override
  Widget build(BuildContext context) {
    return ValueListenableBuilder<bool>(
      valueListenable: _isLoadingNotifier,
      child: child,
      builder: (context, isLoading, child) {
        return Stack(
          children: [
            child!,
            if (isLoading)
              const Opacity(
                opacity: 0.8,
                child: ModalBarrier(dismissible: false, color: Colors.black),
              ),
            if (isLoading)
              const Center(
                child: CircularProgressIndicator(),
              ),
          ],
        );
      },
    );
  }
}
```

We simply initialize the value notifier with a default `false` value, and expose methods that update its value. The `ValueListenableBuilder` will be rebuilt every time the notifier's value changes, so we end up with the same result as doing a `setState` with a stateful widget.

Note that we now use `context.findAncestorWidgetOfExactType<T>()` instead of `context.findAncestorStateOfType<T>()`, as the widget is now stateless, and the `show` and `hide` methods are part of this stateless widget.

Surprisingly (for me, at least!), switching from a stateful to a stateless widget does not really decrease the lines of code in the widget by a lot. And since we initialize the `ValueNotifier` in the constructor, the constructor can no longer be constant. So in the end, this might be less of an improvement but more of a matter of preference.

## Wrapping up
In this tutorial, we created a loading overlay widget and implemented functionality to easily display and hide it, without external packages and less than 70 lines of code. Without the spinner delay and blur filter tweaks, this is even less, at around 50 lines.

You can find the full source code [here](https://github.com/dartling/loading_overlay).

If you found this helpful and would like to be notified of any future tutorials, please sign up with your email [here](https://www.getrevue.co/profile/dartling).