BetterFuture ๐
BetterFuture is a powerful Dart library designed to orchestrate complex asynchronous workflows with ease. It goes beyond Future.wait by providing named results, automatic dependency management, and natural syntax for inter-computation communication.
Key Features โจ
- ๐ฆ Parallel Execution: Run multiple asynchronous computations concurrently.
- โก FutureOr Support: Mix synchronous values and asynchronous futures seamlessly.
- ๐ท๏ธ Named Results: Access results by keys instead of positional indices.
- ๐ Dependency Injection: Use the result of one computation in another within the same block.
- ๐ช Dynamic Syntax: Access results elegantly using
$.keyor$.key<T>(). - ๐ก๏ธ Type Safety: Explicit casting support with built-in primitives and custom type registration.
- ๐งน Automatic Cleanup: Clean up resources if a parallel task fails.
- โก Web & WASM Ready: Fully compatible with JS and WASM compilation targets.
- โก Error Orchestration: Choose between eager or lazy failure.
Installation ๐ฅ
Add better_future to your pubspec.yaml:
dependencies:
better_future: ^1.0.1
Quick Start ๐
import 'package:better_future/better_future.dart';
void main() async {
final results = await BetterFuture.wait({
// Supports both synchronous values and asynchronous futures (FutureOr)
'user_id': () => 42,
// A computation depending on 'user_id'
'profile': ($) async {
// Notice the dynamic type-safe access using $.key<T>()
final id = await $.user_id<int>();
return fetchUserProfile(id);
},
// Another computation running in parallel
'settings': ($) => fetchSettings(),
});
print(results['profile']);
print(results['settings']);
}
Value Destructuring ๐งฉ
Since BetterFuture.wait returns a standard Dart Map, you can use Dart 3's powerful map patterns to destructure results immediately:
final {
'profile': UserProfile profile,
'settings': AppSettings settings,
} = await BetterFuture.wait<dynamic>({
'profile': ($) => fetchProfile(),
'settings': ($) => fetchSettings(),
});
// Now use profile and settings directly!
Advanced Usage ๐ ๏ธ
Dependency Management with $
The BetterResults object (conventionally named $) allows you to await other computations by their keys. If a computation hasn't finished yet, it will be awaited automatically.
'b': ($) async {
final a = await $.a; // Getter syntax (returns dynamic)
final c = await $.c<double>(); // Method syntax with casting
return a + c;
}
Pro Tip ๐ก: If your keys are numeric (e.g.,
'1','2'), you can access them using the$prefix:await $.$1.
Automatic Cleanup
When performing operations that require cleanup (like opening a file or a database transaction), BetterFuture ensures that if any task fails, the successful ones are cleaned up properly.
final results = await BetterFuture.wait<Resource>(
{
'res1': ($) => openResource(1),
'res2': ($) => throw Exception('Oops!'),
},
cleanUp: (res) => res.dispose(), // Called for 'res1' when 'res2' fails
);
Registering Custom Types
To use $.key<MyCustomType>(), you need to register the type first:
BetterFuture.registerType<User>();
// Later...
final user = await $.current_user<User>();
Error Handling
- Eager (Default
false): SeteagerError: trueto fail immediately as soon as any computation throws an error. - Lazy: Wait for all computations to finish (or fail) before throwing the first encountered error.
Getting Detailed Outcomes
BetterFuture also provides BetterFuture.settle<T>(computations).
This static method returns a Map<String, BetterOutcome<T>> which holds the final outcome of each computation. Concrete instances of BetterOutcome<T> can only be:
- either a
BetterSuccess<T>instance (successful computation); - or a
BetterFailure<T>instance (failed computation).
Keep in mind that BetterFuture.settle<T>() will only complete when all computations have completed. It will never complete with an error. But if a computation never completes, BetterFuture.settle<T>() will never complete either.
Best Practices & Considerations โ ๏ธ
๐ Avoid Cyclic Dependencies
Ensure your computations do not have circular dependencies. If computation A awaits B, and B awaits A, the orchestration will deadlock and never complete. Always design your workflows as a Directed Acyclic Graph (DAG).
โก Synchronous vs Asynchronous
BetterFuture supports both synchronous values and FutureOr functions for maximum flexibility. However:
- Synchronous functions run immediately when
BetterFuture.waitis called. - If a computation is CPU-intensive and implemented synchronously, it will block the event loop, potentially causing UI jank.
- Recommendation: Use synchronous functions only for lightweight constants or simple state access. For heavy processing, always offload the work to an
asyncfunction.
Why BetterFuture? ๐ค
While standard Future.wait is useful for simple parallelization, it falls short in complex real-world scenarios:
- Orchestration, Simplified: If task
Bdepends on taskA, you often have to split yourFuture.waitinto multiple stages or nestawaitcalls, which can accidentally serialize tasks that could have run in parallel.BetterFutureturns your computations into a self-organizing dependency graph. - No More Positional Fragility:
Future.waitreturns aList. If you add a new future to the middle of the list, every index after it changes.BetterFutureuses named keys, making your code robust and easy to refactor. - Maximum Flexibility: Handle mixed workloads with ease. Mix synchronous values and asynchronous tasks, manage heterogeneous result types in a single map, and leverage Dart 3 Map patterns for clean, declarative destructuring. The library normalizes synchronization automatically.
- Unified Reliability: Managing resource cleanup when one task in a group fails is difficult.
BetterFuturehandles the "rollback" logic for you via thecleanUphook.
Examples ๐
Check out the example/ folder for focused demonstrations:
main.dart: Simple quick start.orchestration.dart: Complex dependency graphs and timing.cleanup.dart: Resource management and error recovery.destructuring.dart: Dart 3 Map pattern usage.
Inspiration ๐ก
This package is a complete rewrite and evolution of the better-all TypeScript package. It brings a reimagined approach to elegant, object-based asynchronous orchestration for the Flutter and Dart ecosystem.
Support & Sponsorship โ
If you find BetterFuture useful, consider supporting its development:
- Sponsor me on GitHub: github.com/sponsors/d-markey
- Star the repository to help others find it!
Built with โค๏ธ for better Dart development.
Libraries
- better_future
- A library for sophisticated asynchronous orchestration in Dart.