better_future 2.0.3
better_future: ^2.0.3 copied to clipboard
Advanced asynchronous orchestration with named results, and automatic dependency management and cleanup.
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: ^2.0.0
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.
'c': ($) async {
final a = await $.a; // Getter syntax (returns dynamic)
final b = await $.b<double>(); // Method syntax with casting
return a + b;
}
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 #
- Lazy (default): Wait for all computations to finish (or fail) before throwing the first encountered error.
- Eager: Set
eagerError: trueto fail immediately as soon as any computation throws an error.
Getting Detailed Outcomes #
BetterFuture also provides BetterFuture.settle<T>(computations).
This method returns a Map<String, BetterOutcome<T>> that holds the final outcome of each computation. Concrete instances of BetterOutcome<T> can only be:
- a
BetterSuccess<T>instance (successful computation); or - a
BetterFailure<T>instance (failed computation).
BetterFuture.settle<T>() will never complete with an error. Instead, it completes successfully once all computations have completed. Note that if any computation never completes, BetterFuture.settle<T>() will also never complete.
Also note that BetterFuture.settle<T>() does not handle cleanup on errors. When applicable, you must implement proper cleanup to ensure that resources are released.
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.waitorBetterFuture.settleis 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.
๐๏ธ Bundle Size & Performance #
BetterResults relies on Dart's dynamic method and accessor calls via the dynamic type and noSuchMethod(). This enables constructs such as:
'b': (/* dynamic */ $) async {
final a = await $.a<int>();
//...
}
However, this dynamic mechanism prevents the compiler from performing full tree-shaking, as some calls can only be resolved at runtime. This increases bundle size (due to additional retained code) and impacts performance (due to additional runtime resolution steps).
If size or performance are important to you, you can eliminate dynamic calls by explicitly typing $ as BetterResults. In that case, the code becomes slightly more verbose:
'b': (BetterResults $) async {
final a = await $.get<int>('a');
//...
}
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.settle.dart: Working with outcomes.
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.