DEV Community

Daniel Cardona Rojas
Daniel Cardona Rojas

Posted on

Easy dialog management in Flutter

Having worked with dialogs for a while I have encountered multiple scenarios where I wish I could control dialogs programatically through a controller, similar to how other standard widgets like textfields or scrollviews can be controlled.

It would be cool if we could toggle the visibility of a dialog or group of dialogs through some kind of dialog controller.

Having this would allow to define and manage dialogs in a centralized place and hook statemangement to perform the side effect of displaying dialogs.

Solution

Since we can't modify the API of standard widgets like Alert and creating subclasses for all types of alerts would be cumbersome.

The solution proposed here won't look identical to the API you would get with TextField where you can set a controller property on Widget itself.

So let's start by laying out some the characteristics that our dialog controller should have, here are the main ones:

  • Don't disable normal dialog interactions like closing from buttons.
  • Can return data from dialog through the controller or by popping with value
  • Can show and hide multiple times
  • Showing a new dialog will hide the previous

Brief overview

I'll just mention briefly the classes involved and what they do before showing the code.

  • DialogController: Registers a single dialog and gives the ability to toggle its visibility.
  • StyledDialogController<Style>: Registers a multiple dialogs associated with some style type (typically an enum)

Note: Only one dialog can be shown at a time which is what you normally want

Usage

Suppose we have some status property in our state management that we will use to show different types of dialogs.

We might want to display a dialog if this property changes to some especific values.

enum Status { loading, error, success, iddle }
Enter fullscreen mode Exit fullscreen mode

Lets start by creating a styled controller and registering some dialogs in a stateful widget page:

@override
void initState() {
  // ....

  styledDialogController = StyledDialogController<Status>();

  styledDialogController.registerDialogOf(
    style: Status.error,
    builder: showErrorDialog,
  );

  styledDialogController.registerDialogOf(
    style: Status.success,
    builder: showSuccessDialog,
  );

  styledDialogController.registerDialogOf(
    style: Status.loading,
    builder: showLoadingDialog,
  );

  //...
}
Enter fullscreen mode Exit fullscreen mode

These builders are just functions that display a dialog:

Future<void> showLoadingDialog() {
  return showDialog(
    context: context,
    builder: (context) => AlertDialog(
        content: SizedBox(
      height: 32,
      child: Center(child: CircularProgressIndicator()),
    )),
  );
}
Enter fullscreen mode Exit fullscreen mode

With this in place you can now present and hide dialogs with which ever mechanism you wish:

styledDialogController.showDialogWithStyle(Status.error);
Enter fullscreen mode Exit fullscreen mode

or close the dialog with:

styledDialogController.closeVisibleDialog();
// or
styledDialogController.closeVisibleDialog('some return value');
Enter fullscreen mode Exit fullscreen mode

For example hook up state management to show some of these dialogs automatically like this:

// Using mobx
void initState() {
  //...

  mobx.reaction<Status>((_) => _store.status, (newValue) {
    if (newValue == Status.iddle) {
      closeVisibleDialog();
    } else {
      showDialogWithStyle(newValue);
    }
  });

}
Enter fullscreen mode Exit fullscreen mode

How it works

The main idea behind this is to use the normal future returned by showDialog from Flutter in combination with a hidden Completer in a coordinated manner.

This is the most important part:

Future<T?> show<T>() {
  final completer = Completer<T>();

  _dialogCompleter = completer;

  _showDialog().then((value) {
    if (completer.isCompleted) return;
    completer.complete(value);
  });

  return completer.future;
}

void close<T>([T? value]) {
  if (_dialogCompleter?.isCompleted ?? true) return;

  _dialogCompleter?.complete(value);
  _closeDialog();
  _dialogCompleter = null;
}

Enter fullscreen mode Exit fullscreen mode

If a dialogs is dismissed normally its associated future will terminate the completer and with this we respect the normal behavior or dialogs.

If a dialog has not been dismissed since we don't await it and instead return completer future, we remain in a waiting state until the close function is called which will resolve the completer and possibly return a value.

DialogController

import 'dart:async';

typedef DialogShowHandler<T> = Future<T> Function();

class DialogController {
  late DialogShowHandler _showDialog;
  late Function _closeDialog;
  Completer? _dialogCompleter;

  /// Registers the showing and closing functions
  void configureDialog({
    required DialogShowHandler show,
    required Function close,
  }) {
    if (_dialogCompleter != null && !_dialogCompleter!.isCompleted) {
      _closeDialog();
    }
    _showDialog = show;
    _closeDialog = close;
  }

  Future<T?> show<T>() {
    final completer = Completer<T>();

    _dialogCompleter = completer;

    _showDialog().then((value) {
      if (completer.isCompleted) return;
      completer.complete(value);
    });

    return completer.future;
  }

  void close<T>([T? value]) {
    if (_dialogCompleter?.isCompleted ?? true) return;

    _dialogCompleter?.complete(value);
    _closeDialog();
    _dialogCompleter = null;
  }
}

Enter fullscreen mode Exit fullscreen mode

StyledDialogController

StyledDialogController just wraps a DialogController and records different dialog showing functions for each style.

class StyledDialogController<S> {
  Map<String, DialogShowHandler> _styleBuilders = {};

  S? visibleDialogStyle;

  DialogController _dialogController = DialogController();

  void registerDialogOf(
      {required S style, required DialogShowHandler builder}) {
    final styleIdentifier = style.toString();
    _styleBuilders[styleIdentifier] = builder;
  }

  Future<T?> showDialogWithStyle<T>(
    S style, {
    required Function closingFunction,
  }) {
    visibleDialogStyle = style;
    final styleIdentifier = style.toString();
    final builder = _styleBuilders[styleIdentifier];

    if (builder == null) return Future.value(null);

    _dialogController.configureDialog(
      show: builder,
      close: closingFunction,
    );
    return _dialogController.show();
  }

  void closeVisibleDialog<T>([T? value]) {
    visibleDialogStyle = null;
    _dialogController.close(value);
  }
}
Enter fullscreen mode Exit fullscreen mode

Some other features can be built upon this base setup like:

  • preregistering dialog styles on a specialized StyledDialogController and using it through out many pages
  • having a base page type that already includes this ability
  • creating a mixin so multiple pages can show a set of dialogs

Hope you liked this post. Let me know your thoughts on this or if you have other ideas improving this setup.

Discussion (0)