Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,18 @@
## Unreleased

**Breaking:** `ProgressDialogResult` is sealed and now has a third variant, so
exhaustive `switch` statements over it need a `Cancelled` case added.

* Fixed "Null check operator used on a null value" crash when the dialog was
dismissed before the task completed — by the Android back button, or by a
custom `builder` calling `Navigator.pop`
* Added `Cancelled<T>` result variant, `isCancelled`, and
`ProgressDialogResult.cancelled()`
* `unwrap()` throws `ProgressDialogCancelledException` for a cancelled result;
`map()` and `flatMap()` pass cancellation through
* Note: the task is not interrupted on cancellation — Dart futures cannot be
cancelled, so it runs to completion and its result is discarded

## 1.5.0

* Fixed crash when dialog context unmounts before task completes
Expand Down
33 changes: 26 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,13 @@ Developed and maintained by [Nerdy Pro](https://nerdy.pro).

## Features

- Show a non-dismissible progress dialog while a Future is running
- Show a modal progress dialog while a Future is running
- Material and Cupertino dialog styles
- Adaptive dialog that automatically matches the host platform
- Custom dialog UI via a builder parameter
- Type-safe `ProgressDialogResult<T>` with `Success` and `Failure` variants
- Type-safe `ProgressDialogResult<T>` with `Success`, `Failure`, and `Cancelled` variants
- Error handling with stack traces
- Dismissal (back button, custom cancel button) reported as `Cancelled` rather than crashing

## Installation

Expand Down Expand Up @@ -51,6 +52,8 @@ Future<void> onButtonPressed(BuildContext context) async {
print('Got: $value');
case Failure(:final error):
print('Error: $error');
case Cancelled():
print('Dismissed before the task finished');
}
}
```
Expand Down Expand Up @@ -89,10 +92,11 @@ final result = await showProgressDialog(

### Handling results

`ProgressDialogResult<T>` is a sealed class with two variants:
`ProgressDialogResult<T>` is a sealed class with three variants:

- `Success<T>` — contains the `value` returned by the task
- `Failure<T>` — contains the `error` and optional `stackTrace`
- `Cancelled<T>` — the dialog was dismissed before the task delivered a result

```dart
switch (result) {
Expand All @@ -102,16 +106,31 @@ switch (result) {
case Failure(:final error, :final stackTrace):
// Handle the error
break;
case Cancelled():
// The user dismissed the dialog
break;
}
```

### Cancellation

The dialog is modal and ignores taps on the barrier, but it is still a route: the
Android back button pops it, and a custom `builder` may render its own cancel
button that calls `Navigator.pop`. Either way the call returns `Cancelled<T>`.

The task itself is **not** interrupted — Dart futures cannot be cancelled. It runs
to completion in the background and its result, value or error, is discarded. If
the task needs to stop doing work, give it its own cancellation mechanism.

You can also use convenience methods:

```dart
result.isSuccess; // true if Success
result.isError; // true if Failure
result.unwrap(); // returns value or throws error
result.map((v) => v.toString()); // transforms Success value
result.isSuccess; // true if Success
result.isError; // true if Failure
result.isCancelled; // true if Cancelled
result.unwrap(); // returns value, throws the error, or throws
// ProgressDialogCancelledException if cancelled
result.map((v) => v.toString()); // transforms Success value, passes the rest through
```

## API reference
Expand Down
38 changes: 38 additions & 0 deletions example/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,11 @@ Future<String> _successTask() async {
return 'Task completed';
}

Future<String> _slowTask() async {
await Future.delayed(const Duration(seconds: 10));
return 'Task completed';
}

Future<String> _failingTask() async {
await Future.delayed(const Duration(seconds: 2));
throw Exception('Something went wrong');
Expand All @@ -39,6 +44,7 @@ class ExamplePage extends StatelessWidget {
final message = switch (result) {
Success(:final value) => value,
Failure(:final error) => 'Error: $error',
Cancelled() => 'Cancelled',
};

ScaffoldMessenger.of(context)
Expand Down Expand Up @@ -86,6 +92,34 @@ class ExamplePage extends StatelessWidget {
_showResult(context, result);
}

// A custom dialog that offers its own cancel button. Popping the route
// resolves the call with Cancelled; the task keeps running in the background.
Future<void> _onCancellable(BuildContext context) async {
final result = await showProgressDialog(
context: context,
future: () => _slowTask(),
builder: (dialogContext) => Dialog(
child: Padding(
padding: const EdgeInsets.all(24),
child: Column(
mainAxisSize: MainAxisSize.min,
spacing: 16,
children: [
const CircularProgressIndicator(),
const Text('This takes a while...'),
TextButton(
onPressed: () => Navigator.of(dialogContext).pop(),
child: const Text('Cancel'),
),
],
),
),
),
);
if (!context.mounted) return;
_showResult(context, result);
}

Future<void> _onFailure(BuildContext context) async {
final result = await showProgressDialog(
context: context,
Expand Down Expand Up @@ -119,6 +153,10 @@ class ExamplePage extends StatelessWidget {
onPressed: () => _onCustom(context),
child: const Text('Custom dialog'),
),
FilledButton(
onPressed: () => _onCancellable(context),
child: const Text('Cancellable'),
),
FilledButton.tonal(
onPressed: () => _onFailure(context),
child: const Text('Failure'),
Expand Down
58 changes: 54 additions & 4 deletions lib/src/result.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
/// The result of an asynchronous operation shown in a progress dialog.
///
/// Either a [Success] containing the value, or a [Failure] containing the error.
/// One of [Success] containing the value, [Failure] containing the error, or
/// [Cancelled] when the dialog was dismissed before the task delivered a result.
///
/// Example usage:
/// ```dart
Expand All @@ -10,9 +11,13 @@
/// print('Success: $value');
/// case Failure<String>(error: var error, stackTrace: var trace):
/// print('Error: $error');
/// case Cancelled<String>():
/// print('Dismissed before the task finished');
/// }
/// ```
sealed class ProgressDialogResult<T> {
const ProgressDialogResult();

/// Returns true if this result represents an error/failure case, false otherwise.
/// This is equivalent to checking if the result is an instance of [Failure].
bool get isError => this is Failure<T>;
Expand All @@ -21,17 +26,26 @@ sealed class ProgressDialogResult<T> {
/// This is equivalent to checking if the result is an instance of [Success].
bool get isSuccess => this is Success<T>;

/// Returns true if the dialog was dismissed before the task delivered a result.
/// This is equivalent to checking if the result is an instance of [Cancelled].
bool get isCancelled => this is Cancelled<T>;

/// Creates a success result with the given value
static ProgressDialogResult<T> success<T>(T value) => Success(value);

/// Creates a failure result with the given error and optional stack trace
static ProgressDialogResult<T> failure<T>(Object error, [StackTrace? stackTrace]) => Failure(error, stackTrace);

/// Unwraps the result, returning the success value or throwing the error
/// Creates a cancelled result
static ProgressDialogResult<T> cancelled<T>() => Cancelled<T>();

/// Unwraps the result, returning the success value, throwing the error, or
/// throwing a [ProgressDialogCancelledException] if the dialog was dismissed
T unwrap() {
return switch (this) {
Success(:final value) => value,
Failure(:final error) => throw error,
Cancelled() => throw const ProgressDialogCancelledException(),
};
}

Expand All @@ -40,6 +54,7 @@ sealed class ProgressDialogResult<T> {
return switch (this) {
Success(:final value) => Success(fn(value)),
Failure(:final error, :final stackTrace) => Failure(error, stackTrace),
Cancelled() => Cancelled<R>(),
};
}

Expand All @@ -48,6 +63,7 @@ sealed class ProgressDialogResult<T> {
return switch (this) {
Success(:final value) => fn(value),
Failure(:final error, :final stackTrace) => Failure(error, stackTrace),
Cancelled() => Cancelled<R>(),
};
}
}
Expand All @@ -59,7 +75,7 @@ class Success<T> extends ProgressDialogResult<T> {
@override
bool get isError => false;

Success(this.value);
const Success(this.value);

@override
bool operator ==(Object other) =>
Expand All @@ -77,7 +93,7 @@ class Failure<T> extends ProgressDialogResult<T> {
@override
bool get isError => true;

Failure(this.error, [this.stackTrace]);
const Failure(this.error, [this.stackTrace]);

@override
bool operator ==(Object other) =>
Expand All @@ -87,3 +103,37 @@ class Failure<T> extends ProgressDialogResult<T> {
@override
int get hashCode => error.hashCode ^ stackTrace.hashCode;
}

/// A cancelled result: the dialog was dismissed before the task's result could
/// be delivered — for example by the Android back button, or by a custom
/// `builder` that calls `Navigator.pop`.
///
/// The task itself is **not** interrupted. Dart futures cannot be cancelled, so
/// the work keeps running to completion in the background and its result — value
/// or error — is discarded.
class Cancelled<T> extends ProgressDialogResult<T> {
const Cancelled();

@override
bool get isError => false;

@override
bool get isCancelled => true;

@override
bool operator ==(Object other) => identical(this, other) || other is Cancelled && runtimeType == other.runtimeType;

@override
int get hashCode => runtimeType.hashCode;
}

/// Thrown by [ProgressDialogResult.unwrap] when the result is [Cancelled].
///
/// A cancelled result carries neither a value to return nor an error to rethrow,
/// so `unwrap` throws this instead.
class ProgressDialogCancelledException implements Exception {
const ProgressDialogCancelledException();

@override
String toString() => 'ProgressDialogCancelledException: the progress dialog was dismissed before the task completed';
}
34 changes: 26 additions & 8 deletions lib/src/show_progress_dialog.dart
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,11 @@ Future<void> _callback<T>(
/// * [fullscreenDialog] - Whether this dialog is a fullscreen dialog
/// * [animationStyle] - Style of the dialog animation
///
/// Returns a [ProgressDialogResult] containing either the successful result value
/// or error details if the future fails.
/// Returns a [ProgressDialogResult]: [Success] with the value, [Failure] with the
/// error details if the future fails, or [Cancelled] if the dialog is dismissed
/// before the task delivers a result — for example by the Android back button, or
/// by a custom [builder] that calls `Navigator.pop`. The task itself is not
/// interrupted in that case; it runs to completion and its result is discarded.
///
/// Example:
/// ```dart
Expand All @@ -58,6 +61,9 @@ Future<void> _callback<T>(
/// case Failure(error: final error):
/// print('Task failed: $error');
/// break;
/// case Cancelled():
/// print('Dialog dismissed before the task finished');
/// break;
/// }
/// ```
Future<ProgressDialogResult<T>> showProgressDialog<T>({
Expand Down Expand Up @@ -107,7 +113,7 @@ Future<ProgressDialogResult<T>> showProgressDialog<T>({

final result = await navigator.push<ProgressDialogResult<T>>(route);

return result!;
return result ?? Cancelled<T>();
}

/// Shows a Cupertino-styled progress dialog while executing an asynchronous task.
Expand All @@ -125,8 +131,11 @@ Future<ProgressDialogResult<T>> showProgressDialog<T>({
/// * [barrierColor] - Color of the modal barrier behind the dialog
/// * [requestFocus] - Whether the dialog should request focus when opened
///
/// Returns a [ProgressDialogResult] containing either the successful result value
/// or error details if the future fails.
/// Returns a [ProgressDialogResult]: [Success] with the value, [Failure] with the
/// error details if the future fails, or [Cancelled] if the dialog is dismissed
/// before the task delivers a result — for example by the Android back button, or
/// by a custom [builder] that calls `Navigator.pop`. The task itself is not
/// interrupted in that case; it runs to completion and its result is discarded.
///
/// Example:
/// ```dart
Expand All @@ -142,6 +151,9 @@ Future<ProgressDialogResult<T>> showProgressDialog<T>({
/// case Failure(error: final error):
/// print('Task failed: $error');
/// break;
/// case Cancelled():
/// print('Dialog dismissed before the task finished');
/// break;
/// }
/// ```
Future<ProgressDialogResult<T>> showCupertinoProgressDialog<T>({
Expand Down Expand Up @@ -171,7 +183,7 @@ Future<ProgressDialogResult<T>> showCupertinoProgressDialog<T>({
requestFocus: requestFocus,
);
final result = await navigator.push<ProgressDialogResult<T>>(route);
return result!;
return result ?? Cancelled<T>();
}

/// Shows a platform-adaptive progress dialog while executing an asynchronous task.
Expand All @@ -191,8 +203,11 @@ Future<ProgressDialogResult<T>> showCupertinoProgressDialog<T>({
/// * [useSafeArea] - Whether to respect system UI safe areas, defaults to true
/// * [requestFocus] - Whether the dialog should request focus when opened
///
/// Returns a [ProgressDialogResult] containing either the successful result value
/// or error details if the future fails.
/// Returns a [ProgressDialogResult]: [Success] with the value, [Failure] with the
/// error details if the future fails, or [Cancelled] if the dialog is dismissed
/// before the task delivers a result — for example by the Android back button, or
/// by a custom [builder] that calls `Navigator.pop`. The task itself is not
/// interrupted in that case; it runs to completion and its result is discarded.
///
/// Example:
/// ```dart
Expand All @@ -208,6 +223,9 @@ Future<ProgressDialogResult<T>> showCupertinoProgressDialog<T>({
/// case Failure(error: final error):
/// print('Task failed: $error');
/// break;
/// case Cancelled():
/// print('Dialog dismissed before the task finished');
/// break;
/// }
/// ```
Future<ProgressDialogResult<T>> showAdaptiveProgressDialog<T>({
Expand Down
Loading
Loading