Skip to content

Instantly share code, notes, and snippets.

@sma
Created December 16, 2023 14:45
Show Gist options
  • Save sma/febf9b60b04a0e7a14a2b2d4e0262c58 to your computer and use it in GitHub Desktop.
Save sma/febf9b60b04a0e7a14a2b2d4e0262c58 to your computer and use it in GitHub Desktop.
import 'dart:async';
import 'package:flutter/material.dart';
/// See [asyncTask].
typedef Task<T> = Future<T?> Function(bool Function() cancelled);
/// Runs the future returned by [task] and shows a modal waiting dialog after
/// an initial [delay] (default 100 ms) with an optional [label] until the future
/// completes. The [task] is passed a function that returns whether the task
/// has been cancelled by the user. It should periodically check this and return
/// early if it is true.
Future<T?> asyncTask<T>({
required BuildContext context,
required Task<T> task,
Duration? delay,
Widget? title,
Widget? cancelLabel,
}) async {
T? result;
var state = 0;
var isCancelled = false;
return Future.any([
// run the task and set isDone when it completes so we can avoid showing
// the waiting dialog if it completes quickly enough; if it was already
// shown, we close it
task(() => isCancelled).then((value) => result = value).whenComplete(() {
if (state == 1 && context.mounted) {
Navigator.of(context).pop();
}
state = 2;
}),
// show the waiting dialog after the delay; if the user cancels, we set
// isCancelled so the task can return early; also set hasDialog so we
// can close it if the task eventually completes
Future<void>.delayed(delay ?? const Duration(milliseconds: 100), () async {
if (state != 0 || !context.mounted) return;
state = 1;
return showDialog<void>(
context: context,
barrierDismissible: false,
builder: (_) => AlertDialog(
title: title,
content: const IntrinsicHeight(child: Center(child: CircularProgressIndicator())),
actions: [
TextButton(
onPressed: () => isCancelled = true,
child: cancelLabel ?? const Text('Cancel'),
),
],
),
);
}),
]).then((_) => result);
}
void main() {
runApp(const App());
}
class App extends StatelessWidget {
const App({super.key});
@override
Widget build(BuildContext context) {
return const MaterialApp(
home: Page(),
);
}
}
class Page extends StatelessWidget {
const Page({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: ElevatedButton(
onPressed: () async {
final result = await asyncTask(
context: context,
task: (isCancelled) async {
for (var i = 0; i < 40; i++) {
await Future<void>.delayed(const Duration(milliseconds: 90));
if (isCancelled()) return null;
}
return 42;
},
title: const Text('Waiting…'),
);
print('result: $result');
},
child: const Text('Run Task'),
),
));
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment