|
16 | 16 |
|
17 | 17 | - Ease of use: |
18 | 18 | - Built-in dependency injection and service locators. |
| 19 | + - Built-in optimistic update functionality. |
19 | 20 | - Boasts simple `Alerter` and `Digester` widgets for managing UI layer reactively. |
20 | 21 | - Fine-grained control: |
21 | 22 | - Includes special shorthand `watch<T>`, `watchMap<T, S>`, and `get<T>` functions for reduced-boilerplate managing of UI layer. |
@@ -438,6 +439,91 @@ class CalculatorTrent extends Trent<CalculatorStates> { |
438 | 439 | } |
439 | 440 | ``` |
440 | 441 |
|
| 442 | +### Optimistic Updates |
| 443 | + |
| 444 | +Optimistic updates let you immediately reflect a change in your UI before an async operation (like a network request) completes. This makes your app feel faster and more responsive. If the async operation fails, the optimistic update can be reverted. |
| 445 | + |
| 446 | +Trent provides a built-in API for optimistic updates via the `optimisticUpdate<T>(...)` method, available on any Trent instance. Optimistic updates are tracked by tag, can be explicitly accepted or rejected, and are automatically cleaned up on `reset()`. |
| 447 | + |
| 448 | +#### Tags |
| 449 | +A tag is a string that identifies the optimistic update. If you run multiple optimistic updates with the same tag, only the latest effect for that tag is present; previous effects are fully reverted before the new one is applied. This ensures repeated or colliding updates don't stack up and cause state confusion. If you don't provide a tag, Trent will generate a universally unique tag for you. |
| 450 | + |
| 451 | +```dart |
| 452 | +// In your Trent subclass: |
| 453 | +optimisticUpdate<int>( |
| 454 | + tag: "counter", |
| 455 | + forward: (state, value) => state.copyWith(value: state.value + value), |
| 456 | + reverse: (state, value) => state.copyWith(value: state.value - value), |
| 457 | +).execute(5); // Immediately applies +5 optimistically |
| 458 | +``` |
| 459 | + |
| 460 | +#### Resolving Optimistic Updates |
| 461 | +There are three main ways to resolve an optimistic update: |
| 462 | + |
| 463 | +1. **Accept**: Call `accept()` on the attempt to confirm the optimistic update (e.g., after a successful async operation). This makes the update permanent and removes it from the pending list. |
| 464 | +2. **Accept As**: Call `acceptAs(newValue)` to accept the update but with a new value (runs revert, then applies the new value as a new optimistic update). |
| 465 | +3. **Reject**: Call `reject()` to revert the optimistic update (e.g., if the async operation fails). This will undo the effect and remove it from the pending list. |
| 466 | + |
| 467 | +All of these methods are available on the returned `OptimisticAttempt`: |
| 468 | + |
| 469 | +```dart |
| 470 | +final attempt = optimisticUpdate<int>( |
| 471 | + tag: "counter", |
| 472 | + forward: (state, value) => state.copyWith(value: state.value + value), |
| 473 | + reverse: (state, value) => state.copyWith(value: state.value - value), |
| 474 | +); |
| 475 | +attempt.execute(10); // Apply +10 optimistically |
| 476 | +
|
| 477 | +// Later, after async completes: |
| 478 | +attempt.accept(); // Accepts and finalizes |
| 479 | +// or |
| 480 | +attempt.reject(); // Reverts the update |
| 481 | +// or |
| 482 | +attempt.acceptAs(42); // Reverts +10, then applies +42 |
| 483 | +``` |
| 484 | + |
| 485 | +Optimistic updates are async-safe: if you run multiple with the same tag before accepting, acceptingAs, or rejecting, only the latest is kept and previous ones are reverted before the new one is applied. This prevents flooding/collision issues. |
| 486 | + |
| 487 | +If you always resolve each optimistic attempt (by calling `accept()`, `acceptAs(...)`, or `reject()` on every attempt you create), you will not leak memory or state. The additional cleanup methods (like `rejectAllUnresolvedOptimisticUpdates`) are provided as extra safety for cases where you might forget to resolve, or for bulk cleanup after network failures, app suspends, or other edge cases. |
| 488 | + |
| 489 | +#### Cleanup |
| 490 | + |
| 491 | +To prevent memory leaks or stale updates, Trent provides a cleanup method: |
| 492 | + |
| 493 | +- `rejectAllUnresolvedOptimisticUpdates({Duration? olderThan})`: Rejects all unresolved optimistic updates, or only those older than a given duration. This is useful for cleaning up after network failures, app suspends, or just to ensure your state is fresh. |
| 494 | + |
| 495 | +```dart |
| 496 | +// Reject all unresolved optimistic updates |
| 497 | +trent.rejectAllUnresolvedOptimisticUpdates(); |
| 498 | +
|
| 499 | +// Reject only those older than 30 seconds |
| 500 | +trent.rejectAllUnresolvedOptimisticUpdates(olderThan: Duration(seconds: 30)); |
| 501 | +``` |
| 502 | + |
| 503 | +Optimistic updates are also automatically cleaned up on a Trent's `reset(...)` being called. |
| 504 | + |
| 505 | +**Example:** |
| 506 | + |
| 507 | +```dart |
| 508 | +final attempt = optimisticUpdate<int>( |
| 509 | + tag: "saveDraft", |
| 510 | + forward: (state, value) => state.copyWith(value: value), |
| 511 | + reverse: (state, value) => state.copyWith(value: state.value - value), |
| 512 | +); |
| 513 | +attempt.execute(123); |
| 514 | +
|
| 515 | +// If the save fails after 10 seconds |
| 516 | +Future.delayed(Duration(seconds: 10), () { |
| 517 | + attempt.reject(); |
| 518 | +}); |
| 519 | +
|
| 520 | +// Or, to clean up all stale attempts after a while |
| 521 | +trent.rejectAllUnresolvedOptimisticUpdates(olderThan: Duration(seconds: 10)); |
| 522 | +``` |
| 523 | + |
| 524 | +This system ensures your UI remains responsive, your state stays consistent, and you have full control over optimistic updates and their lifecycle. |
| 525 | + |
| 526 | + |
441 | 527 | ## How to Use |
442 | 528 |
|
443 | 529 | ### 1. Define Your State Types |
|
0 commit comments