safe_effect
safe_effect is a lightweight Flutter utility for safely executing side-effects
(API calls, payments, analytics, sync logic) without accidentally running them
multiple times due to widget rebuilds or app lifecycle changes.
Flutter rebuilds widgets aggressively and provides no built-in guarantees around how often imperative logic is executed. As a result, side-effects are frequently triggered more than intended.
This package introduces explicit, lifecycle-aware guards so that critical side-effects run only when intended and only as often as intended.
Features
- Execute side-effects only once per defined scope
- Prevent duplicate execution caused by:
- widget rebuilds
- navigation
- app background → foreground transitions
- Lifecycle-aware execution on app resume
- Configurable execution scopes:
screensessionapp(persistent)
- Optional persistence using
SharedPreferences - Minimal API with no dependency on state-management solutions
Getting started
Prerequisites
- Flutter SDK
- Dart 3.0 or later
Installation
Add the dependency to your pubspec.yaml:
dependencies:
safe_effect: ^0.0.1
## Usage
### Run a side-effect only once
This is the most common use case.
Even if the widget rebuilds multiple times, the effect will execute only once
for the selected scope.
```dart
SafeEffect.runOnce(
key: 'fetch_profile',
scope: EventScope.session,
effect: fetchUserProfile,
);
Conditional execution
You can prevent execution unless a condition is satisfied.
SafeEffect.runOnce(
key: 'payment_init',
scope: EventScope.session,
condition: () => cart.totalAmount > 0,
effect: initiatePayment,
);
Execution scopes
safe_effect supports different lifetimes for guarded execution:
-
EventScope.screen Resets when the widget tree is disposed.
-
EventScope.session Resets when the app process is killed.
-
EventScope.app Persists across app restarts (requires persistent store).
Example:
SafeEffect.runOnce(
key: 'onboarding_shown',
scope: EventScope.app,
effect: showOnboarding,
);
Execute logic on app resume
Useful for syncing data or refreshing state when the user returns to the app.
SafeEffect.onResume(
key: 'sync_subscription',
debounce: Duration(seconds: 10),
effect: syncSubscriptionStatus,
);
The optional debounce prevents rapid repeated executions when the app is
resumed frequently.
Enable persistent guarding (optional)
To persist guarded execution across app restarts, configure a persistent store:
final prefs = await SharedPreferences.getInstance();
SafeEffect.configurePersistentStore(
PersistentGuardStore(prefs),
);
Reset a guarded effect
You may explicitly allow a guarded effect to run again:
await SafeEffect.reset(
'fetch_profile',
EventScope.session,
);
For a complete runnable example, see the /example directory.
Additional information
When should I use this package?
Use safe_effect when:
- an API call must not be triggered multiple times
- a payment or checkout flow must be protected
- analytics events should not be duplicated
- resume-based sync logic should be controlled
When should I NOT use this package?
This package is intentionally not designed for:
- retrying failed network requests
- background execution
- task queues or job schedulers
- state management
Contributing
Contributions are welcome. Please open an issue before submitting large changes so behavior and scope can be discussed.
Issues and support
- Bugs and feature requests can be filed via GitHub issues