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 }
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,
);
//...
}
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()),
)),
);
}
With this in place you can now present and hide dialogs with which ever mechanism you wish:
styledDialogController.showDialogWithStyle(Status.error);
or close the dialog with:
styledDialogController.closeVisibleDialog();
// or
styledDialogController.closeVisibleDialog('some return value');
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);
}
});
}
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;
}
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;
}
}
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);
}
}
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.
Top comments (0)