flutter_future_progress_dialog
Show a progress dialog in Flutter while a Future runs, then get the result back — Material, Cupertino, and platform-adaptive styles with type-safe results.

flutter_future_progress_dialog is a Flutter package that displays a modal loading dialog for the lifetime of an asynchronous task and returns a type-safe ProgressDialogResult<T> describing the outcome: Success, Failure, or Cancelled. A single await replaces the usual show-dialog, try-catch, pop-dialog boilerplate.
Developed and maintained by Nerdy Pro.
Contents
- Quick start
- Why use flutter_future_progress_dialog
- Features
- Requirements and platform support
- Installation
- Usage
- API reference
- FAQ
- Migrating from 1.x
- Known limitations
- License
Quick start
Install the package, then wrap any Future in showProgressDialog:
import 'package:flutter_future_progress_dialog/flutter_future_progress_dialog.dart';
Future<String> fetchData() async {
await Future.delayed(const Duration(seconds: 2));
return 'Hello';
}
Future<void> onButtonPressed(BuildContext context) async {
final result = await showProgressDialog(
context: context,
future: fetchData,
);
switch (result) {
case Success(:final value):
print('Got: $value');
case Failure(:final error):
print('Error: $error');
case Cancelled():
print('Dismissed before the task finished');
}
}
The dialog appears immediately, stays on screen until fetchData completes, and closes itself. The await resolves with the result.
Why use flutter_future_progress_dialog
Showing a loading dialog around an async call in plain Flutter means opening a route, remembering to close it on every exit path, and guarding the BuildContext across the await:
// Without the package
Future<void> onButtonPressed(BuildContext context) async {
showDialog(
context: context,
barrierDismissible: false,
builder: (_) => const Center(child: CircularProgressIndicator()),
);
try {
final value = await fetchData();
if (context.mounted) Navigator.of(context).pop();
print('Got: $value');
} catch (error) {
if (context.mounted) Navigator.of(context).pop();
print('Error: $error');
}
}
// With flutter_future_progress_dialog
Future<void> onButtonPressed(BuildContext context) async {
final result = await showProgressDialog(context: context, future: fetchData);
// result is Success, Failure, or Cancelled — the dialog is already closed
}
The dialog is closed for you on every path, errors are captured instead of escaping, and the compiler forces you to handle each outcome because ProgressDialogResult<T> is a sealed class.
Use flutter_future_progress_dialog when you have a discrete async task — a network call, a file write, a sign-in — that should block interaction until it finishes.
Use something else when you need determinate progress (a percentage bar), inline loading state inside a page rather than a modal, or a task the user can genuinely abort mid-flight. See Known limitations.
Features
- Show a modal progress dialog while a
Futureis running, and get its result back - Material, Cupertino, and platform-adaptive dialog styles
- Custom dialog UI through a
builderparameter - Type-safe
ProgressDialogResult<T>withSuccess,Failure, andCancelledvariants - Errors captured with stack traces instead of thrown across the dialog boundary
- Dismissal — Android back button, or a custom cancel button — reported as
Cancelledrather than crashing - Nullable and
voidtask types fully supported: a task returningnullyieldsSuccess(null), neverCancelled
Requirements and platform support
| Dart SDK | ^3.0.0 — Dart 3 is required for sealed classes and pattern matching |
| Dependencies | None beyond the Flutter SDK |
| License | MIT |
Supported platforms, as reported on pub.dev:
| Platform | Supported |
|---|---|
| Android | Yes |
| iOS | Yes |
| macOS | Yes |
| Windows | Yes |
| Linux | Yes |
| Web | Yes |
Installation
flutter pub add flutter_future_progress_dialog
Or add it to pubspec.yaml manually:
dependencies:
flutter_future_progress_dialog: ^2.0.0
Then import it:
import 'package:flutter_future_progress_dialog/flutter_future_progress_dialog.dart';
Usage
How do I show a loading dialog while a Future runs in Flutter?
Call showProgressDialog with a context and a future callback. showProgressDialog displays a Material dialog containing a CircularProgressIndicator, keeps it on screen until the task completes, closes it, and resolves with a ProgressDialogResult<T>.
final result = await showProgressDialog(
context: context,
future: () => fetchData(),
);
Note that future takes a callback returning a Future, not a Future itself. The task starts when the dialog is on screen.
How do I show an iOS-style progress dialog?
Call showCupertinoProgressDialog for an iOS-styled dialog built around CupertinoActivityIndicator, or showAdaptiveProgressDialog to select the style automatically — Cupertino on iOS and macOS, Material everywhere else.
showAdaptiveProgressDialog reads Theme.of(context).platform, the same signal Flutter's own showAdaptiveDialog uses. An app that overrides ThemeData.platform gets the style it asked for, and the selection works on Flutter Web.
// Always iOS style
final result = await showCupertinoProgressDialog(
context: context,
future: () => fetchData(),
);
// Cupertino on iOS/macOS, Material elsewhere
final result = await showAdaptiveProgressDialog(
context: context,
future: () => fetchData(),
);
How do I use a custom progress dialog widget?
Pass a builder to replace the default indicator with any widget. The dialog is still driven by the same task lifecycle.
final result = await showProgressDialog(
context: context,
future: () => fetchData(),
builder: (context) => const AlertDialog(
content: Text('Loading, please wait...'),
),
);
How do I handle the result?
ProgressDialogResult<T> is a sealed class with three variants, so a switch over it is checked for exhaustiveness at compile time:
| Variant | Meaning | Carries |
|---|---|---|
Success<T> |
The task completed | value |
Failure<T> |
The task threw | error, stackTrace |
Cancelled<T> |
The dialog was dismissed before the task delivered a result | nothing |
switch (result) {
case Success(:final value):
// Use the value
break;
case Failure(:final error, :final stackTrace):
// Handle the error
break;
case Cancelled():
// The user dismissed the dialog
break;
}
Convenience members are available when a full switch is more than you need:
result.isSuccess; // true if Success
result.isError; // true if Failure
result.isCancelled; // true if Cancelled
result.unwrap(); // returns the value, throws the error, or throws
// ProgressDialogCancelledException if cancelled
result.map((v) => v.toString()); // transforms a Success value, passes the rest through
result.flatMap((v) => otherResult); // chains results
Can the user cancel the progress dialog?
The dialog ignores taps on the modal barrier, but it is still a route. The Android back button pops it, and a custom builder can render its own cancel button that calls Navigator.pop. Either way, the await resolves with Cancelled<T> instead of crashing.
final result = await showProgressDialog(
context: context,
future: () => slowUpload(),
builder: (dialogContext) => Dialog(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const CircularProgressIndicator(),
TextButton(
onPressed: () => Navigator.of(dialogContext).pop(),
child: const Text('Cancel'),
),
],
),
),
);
The task itself is not interrupted. Dart futures cannot be cancelled, so the work runs to completion in the background and its result — value or error — is discarded. If the work must actually stop, give the task its own cancellation mechanism, such as a CancelToken on your HTTP client.
API reference
Functions
| Function | Dialog style | Returns |
|---|---|---|
showProgressDialog<T> |
Material — Dialog with a CircularProgressIndicator |
Future<ProgressDialogResult<T>> |
showCupertinoProgressDialog<T> |
Cupertino — CupertinoPopupSurface with a CupertinoActivityIndicator |
Future<ProgressDialogResult<T>> |
showAdaptiveProgressDialog<T> |
Cupertino on iOS and macOS, Material elsewhere, per Theme.of(context).platform |
Future<ProgressDialogResult<T>> |
Parameters
| Parameter | Type | Default | Available on |
|---|---|---|---|
context |
BuildContext |
required | all |
future |
Future<T> Function() |
required | all |
builder |
WidgetBuilder? |
null |
all |
useRootNavigator |
bool |
true |
all |
anchorPoint |
Offset? |
null |
all |
barrierLabel |
String? |
null |
all |
barrierColor |
Color? |
Material: dialog theme, then Colors.black54. Cupertino: the Cupertino default |
all |
requestFocus |
bool? |
null |
all |
useSafeArea |
bool |
true |
Material, adaptive |
traversalEdgeBehavior |
TraversalEdgeBehavior? |
closedLoop |
Material, adaptive |
fullscreenDialog |
bool |
false |
Material |
animationStyle |
AnimationStyle? |
null |
Material |
Types
| Type | Description |
|---|---|
ProgressDialogResult<T> |
Sealed result type; one of Success<T>, Failure<T>, Cancelled<T> |
Task<T> |
Typedef for Future<T> Function(), the shape of the future parameter |
ProgressDialogCancelledException |
Thrown by unwrap() when the result is Cancelled |
A complete working app covering the Material, Cupertino, custom, cancellable, and failure cases is in the example directory.
FAQ
Does dismissing the dialog cancel the Future?
No. Dismissing the dialog resolves the await with Cancelled<T>, but the underlying task keeps running to completion in the background and its result is discarded. Dart futures have no built-in cancellation, so flutter_future_progress_dialog cannot interrupt work already in flight.
Can the user dismiss the progress dialog by tapping outside it?
No. flutter_future_progress_dialog sets barrierDismissible: false, so taps on the modal barrier are ignored. The Android back button still pops the dialog, which yields Cancelled<T>.
How do I handle errors thrown by the task?
Errors are caught for you. If the task throws, the dialog closes and the call resolves with Failure<T>, carrying the error and its stackTrace. Nothing is rethrown across the await, so a try/catch around showProgressDialog is unnecessary.
Does it work with Future<void> or a nullable type?
Yes. A Future<void> task yields Success<void>, and a task returning null yields Success<T>(null) — a null value is never confused with cancellation, because Cancelled is a distinct type rather than an absent value.
Does flutter_future_progress_dialog support Flutter Web?
Yes. flutter_future_progress_dialog supports all six Flutter platforms — Android, iOS, macOS, Windows, Linux, and Web. Platform detection in showAdaptiveProgressDialog goes through Theme.of(context).platform rather than dart:io, so nothing is host-specific.
What is the difference between the three functions?
showProgressDialog always renders a Material dialog, showCupertinoProgressDialog always renders an iOS-style dialog, and showAdaptiveProgressDialog picks between them based on Theme.of(context).platform. All three take the same task callback and return the same ProgressDialogResult<T>.
Do I need to check context.mounted after awaiting?
Yes, if you use the BuildContext afterwards. showProgressDialog closes its own dialog, but your context may still have been unmounted while the task was running, so guard any subsequent use of it as you would after any await.
Migrating from 1.x
Version 2.0.0 has two breaking changes.
ProgressDialogResult has a third variant
Cancelled<T> was added so that dismissing the dialog reports an outcome instead
of crashing. ProgressDialogResult is sealed, so any exhaustive switch over a
result now fails to compile with a message like:
The type 'ProgressDialogResult<String>' isn't exhaustively matched by the switch
cases since it doesn't match the pattern 'Cancelled<String>()'.
Add the missing arm:
switch (result) {
case Success(:final value):
// ...
case Failure(:final error):
// ...
case Cancelled(): // <-- add this
// The dialog was dismissed before the task delivered a result
}
If you would rather not handle cancellation distinctly, treat it like a failure:
if (result.isCancelled) return;
Code using isSuccess, isError, unwrap(), map(), or flatMap() keeps
compiling unchanged. Be aware that unwrap() now throws
ProgressDialogCancelledException for a cancelled result, and that a Success
holding null is still a Success — it is never reported as Cancelled.
showAdaptiveProgressDialog follows the theme, not the host OS
The style is now chosen from Theme.of(context).platform, matching Flutter's own
showAdaptiveDialog. Previously it read the host operating system through
dart:io.
This only changes behaviour for apps that override ThemeData.platform — those
now get the dialog style they asked for. It is also what makes the package work
on Flutter Web, where the old check threw. To pin one style regardless of
platform, call showProgressDialog or showCupertinoProgressDialog directly.
Known limitations
- The task cannot be aborted. Cancellation dismisses the dialog, not the work. See Can the user cancel the progress dialog?
- No determinate progress. The dialog shows an indeterminate spinner; there is no percentage or step reporting.
Contributing
Issues and pull requests are welcome at github.com/nerdy-pro/flutter-progress-dialog.
License
MIT License. See LICENSE for details.