async_redux
The modern version of Redux. State management that's simple to learn and easy to use; Powerful enough to handle complex applications with millions of users; Testable.
28.1.0
7.9K downloads/mo
#3252 most downloaded on pub.dev
marcglasberg/async_redux
What this package is like to depend on
Last release 1 months ago
20 Jul 2026
Ships unpredictably
gaps range from 2 weeks to 6 months
Some releases are documented
notes for 71 of 229 stable releases
1 version withdrawn
withdrawn after publishing
7 years old
269 releases · first in 2019
26 releases in the last 12 months
see the full history below
Release timeline
269 releases · Aug 2019 to Jul 2026Releases
latest 60 of 269-
28.1.020 Jul 2026Release notes
Open source →-
New extension methods
thenIfCompletedOkandthenIfCompletedFailedonFuture<ActionStatus>, which is the type returned bydispatchAndWait. They let you chain code that should run only if the action completed OK, or only if it failed. This is important becausedispatchAndWaitcompletes with anActionStatuseven when the action fails, so a plain.then()would run regardless of success or failure:// The `PollBlockNumber` action will be dispatched only // if `InitializeWeb3` succeeds: dispatchAndWait(InitializeWeb3()) .thenIfCompletedOk((_) => dispatch(PollBlockNumber())); // You can chain both, to handle success and failure: dispatchAndWait(InitializeWeb3()) .thenIfCompletedOk((_) => dispatch(PollBlockNumber())) .thenIfCompletedFailed((status) => log(status.wrappedError));
-
-
28.0.007 Jul 2026Release notes
Open source →-
DEPRECATION WARNING:
Store.globalWrapErrorandStore.errorObserverare now deprecated. Use the newglobalErrorObserverinstead. -
You can now provide a global error observer using the
globalWrapErrorparameter in theStoreconstructor:var store = Store<AppState>( initialState: AppState(), globalErrorObserver: (store) => MyGlobalErrorObserver(), } class MyGlobalErrorObserver extends GlobalErrorObserver { @override void wrap() { // Here you can use: // `error` -> Thrown by the action, AFTER being processed by the action's `wrapError`. // `originalError` -> Error BEFORE being processed by the action's `wrapError`. // `stackTrace` -> The stack trace of the error. // `action` -> The action that threw the error. // `store` -> Use it to read the `store.environment` or `store.configuration`. } }Use cases
-
Use this to set up your app to use 3rd-party services like Sentry or Firebase Crashlytics to monitor your app for errors in production, and print them to the console in development and testing. Since you are setting it up in a centralized way, you don't have to "pollute" your code with logging calls.
-
Use this to have a global place to convert some exceptions into
UserExceptions. For example, Firebase may throw somePlatformExceptions in response to a bad connection to the server. In this case, you may want to show the user a dialog explaining that the connection is bad, which you can do by converting it to aUserException. Note, this could also be done in theReduxAction.wrapError, but then you'd have to add it to all actions that use Firebase.
-
-
BREAKING: Removed the deprecated
Store.wrapError. Use the newglobalErrorObserverinstead. -
BREAKING: Removed the deprecated:
ActionStatus.isBeforeDone(replace withhasFinishedMethodBefore)isReduceDone(replace withhasFinishedMethodReduce)isAfterDone(replace withhasFinishedMethodAfter)isFinished(replace withisBeforeDone && isReduceDone && isAfterDone)
-
-
28.0.0-dev.327 Apr 2026 pre-releaseNothing published for this version
-
28.0.0-dev.202 Mar 2026 pre-releaseNothing published for this version
-
28.0.0-dev.102 Mar 2026 pre-releaseNothing published for this version
-
27.1.127 Feb 2026Release notes
Open source →-
Added
store.removeError(source)to removeUserExceptionerrors from the error queue. You can pass it aUserException, anActionStatus, or aReduxAction. This is sometimes useful in tests. For example:// Dispatch some action var status = await store.dispatchAndWait(SomeAction()); // Check the action failed as expected expect(status.originalError, isError<CloudException>('Insufficient balance.')); // Make sure there are no more errors store.removeError(status); expect(store.errors, isEmpty); -
ActionStatus.contextnow has a reference to the action and the store.
-
-
27.1.027 Feb 2026Nothing published for this version
-
27.0.020 Feb 2026Release notes
Open source →-
BREAKING: This version is only a breaking change if you are using the
enviromentparameter of theStoreconstructor to do dependency injection.The
Storeconstructor now acceptsdependenciesandconfigurationparameters, in addition toenvironment. See filemain_dependency_injection.dartin theexampledirectory for an example.This provides for very granular dependency injection, for all app needs:
-
environment: Specifies if the app is running in production, staging, development, testing, etc. Should be immutable and not change during app execution. Example:enum Environment { production, staging, testing; bool get isProduction => this == Environment.production; bool get isStaging => this == Environment.staging; bool get isTesting => this == Environment.testing; }-
dependencies: A container for injected dependencies (like services, repositories, APIs, etc.), created via a factory that receives theStore, so it can vary based on the environment and/or the configuration. Example:abstract class Dependencies { factory Dependencies(Store store) { if (store.environment == Environment.production) { return DependenciesProduction(); } else if (store.environment == Environment.staging) { return DependenciesStaging(); } else { return DependenciesTesting(); } } }
-
-
configuration: For feature flags and other configuration values.class Config { // Add whatever configuration values you need here, if any. bool isABtestingOn = false; bool showAdminConsole = false; ... } -
configuration: For feature flags and other configuration values.
-
This is how you create a store with these three parameters:
store = Store<AppState>( initialState: AppState.initial(), environment: Environment.production, dependencies: (store) => Dependencies(store), configuration: (store) => Configuration(store), );-
BREAKING:
Store.envhas been renamed toStore.environment. -
BREAKING: Removed
ReduxAction.env. Access it throughstore.environmentinstead. It's recommended to define a typed getter in your base action class:abstract class Action extends ReduxAction<AppState> { Dependencies get dependencies => super.store.dependencies as Dependencies; Environment get environment => super.store.environment as Environment; Config get config => super.store.configuration as Config; } -
BREAKING: Removed
VmFactory.env. Access dependencies throughstore.dependenciesinstead. Define a typed getter in your base factory class:abstract class AppFactory<T extends Widget?, Model extends Vm> extends VmFactory<AppState, T, Model> { AppFactory([T? connector]) : super(connector); Dependencies get dependencies => store.dependencies as Dependencies; Environment get environment => store.environment as Environment; Config get config => store.configuration as Config; } -
Final thoughts: Why is AsyncRedux now providing dependency injection features? The reason is testing. When you create a store in a test, you provide the environment, dependencies, and configuration as parameters. As soon as the test ends, and the store is disposed, the environment, dependencies and configuration are disposed with it. This makes tests less verbose and less prone to memory leaks.
-
-
26.4.215 Feb 2026Release notes
Open source →-
Added the
Pollingmixin andPollenum.Use this mixin to periodically dispatch an action at a fixed interval, keeping data fresh by fetching it from a server. This is useful for refreshing prices, checking for new messages, or monitoring wallet balances.
Control polling with the
Pollenum:Poll.startto begin polling (also runs the action immediately),Poll.stopto cancel it,Poll.runNowAndRestartto run immediately and restart the timer, andPoll.onceto run immediately without affecting the timer.The default interval is 10 seconds, but you can override
pollInterval.class PollPrices extends AppAction with Polling { @override final Poll poll; PollPrices({this.poll = Poll.start}); @override ReduxAction<AppState> createPollingAction() => PollPrices(); @override Future<AppState?> reduce() async { final prices = await api.getPrices(); return state.copy(prices: prices); } } // Start polling (also runs reduce immediately): dispatch(PollPrices()); // Stop polling: dispatch(PollPrices(poll: Poll.stop));Flexible architecture:
You can use a single action for both polling control and work, or separate them into two action types. For example, you could have a
ControlPricePollingaction that only starts/stops the polling, and a separateFetchPricesaction that does the actual fetching.
-
-
26.4.115 Feb 2026Nothing published for this version
-
26.4.013 Feb 2026Nothing published for this version
-
26.3.329 Jan 2026Release notes
Open source →- Added Claude Code Skills to help developers use
async_reduxwith AI assistants. See: https://github.com/marcglasberg/async_redux/tree/master/.claude/skills
- Added Claude Code Skills to help developers use
-
26.3.229 Jan 2026Nothing published for this version
-
26.3.129 Jan 2026Nothing published for this version
-
26.3.029 Jan 2026Nothing published for this version
-
26.2.218 Jan 2026 -
26.2.115 Jan 2026 -
26.2.024 Dec 2025Release notes
Open source →-
Added the
OptimisticCommandmixin.Use this mixin for command-based operations where you want to optimistically update the UI immediately, send a command to the server, and automatically rollback if the server request fails.
This is useful for blocking user interactions like adding a todo item, deleting a record, or updating user settings, where you want instant UI feedback but also need to ensure consistency with the server.
It's blocking in the sense that the user cannot perform other operation in the same state until the command completes (success or failure).
See file
example/lib/main_optimistic_command.dartfor an example app demonstrating the use ofOptimisticCommandin a like button.class SaveTodo extends AppAction with OptimisticCommand { final Todo newTodo; SaveTodo(this.newTodo); // The new Todo is going to be optimistically applied to the state, right away. @override Object? optimisticValue() => newTodo; // We teach the action how to read the Todo from the state. @override Object? getValueFromState(AppState state) => state.todoList.getById(newTodo.id); // We teach the action how to add the new Todo to the state. @override AppState applyValueToState(AppState state, Object? value) => state.copy(todoList: state.todoList.add(newTodo)); // Contact the server to send the command (save the Todo). I @override Future<Todo> sendCommandToServer(Object? newTodo) async => await saveTodo(newTodo); // If the server returns a value, we may apply it to the state. @override AppState applyServerResponseToState(AppState state, Todo todo) => state.copy(todoList: state.todoList.add(todo)); // Reload from the cloud (in case of error). @override Future<Object?> reloadFromServer() async => await loadTodo(); }Key features:
-
Instant UI update: The state is updated immediately when the action is dispatched, before the server request completes.
-
Automatic rollback: If
sendCommandToServerfails, the mixin checks if the current state still contains the optimistic value. If so, it safely rolls back to the initial value. -
Non-reentrant by default: Concurrent dispatches of the same action type are prevented. Use
nonReentrantKeyParams()to allow parallel execution for different parameters (e.g., different item IDs). -
Optional reload: Override
reloadFromServer()to fetch fresh data from the server after the command completes (success or failure).
-
-
Added the
OptimisticSyncmixin.Use this mixin for non-blocking user interactions where you want instant UI feedback and automatic synchronization with the server. It's non-blocking in the sense that the user can continue performing other operations in the same state while synchronization is in progress. The mixin handles rapid user interactions gracefully by coalescing requests and ensuring eventual consistency.
This is ideal for toggle buttons (like/unlike, follow/unfollow), sliders, switches, or any control where the user might interact multiple times before the server responds.
See file
example/lib/main_optimistic_sync.dartfor an example app demonstrating the use ofOptimisticSyncin a like button.class ToggleLike extends ReduxAction<AppState> with OptimisticSync<AppState, bool> { final String itemId; ToggleLike(this.itemId); // Differentiate by item ID so different items can sync independently. Object? optimisticSyncKeyParams() => itemId; // The value to apply optimistically (toggle current state). bool valueToApply() => !state.items[itemId].isLiked; // Apply the value to the state. AppState applyOptimisticValueToState(AppState state, bool isLiked) => state.copyWith(items: state.items.setLiked(itemId, isLiked)); // Get the current value from the state. bool getValueFromState(AppState state) => state.items[itemId].isLiked; // Send the value to the server. Future<Object?> sendValueToServer(Object? value) async { var response = await api.setLiked(itemId, value as bool); return response.liked; // Return server-confirmed value, or null. } // Apply server response to the state (optional). AppState? applyServerResponseToState(AppState state, Object response) => state.copyWith(items: state.items.setLiked(itemId, response as bool)); }How it works:
-
Optimistic update: When dispatched, the UI is updated immediately.
-
Request coalescing: If the user interacts again while a request is in flight, the new value is applied to the UI but no new request is sent yet. The mixin waits for the current request to complete.
-
Follow-up requests: After the request completes, the mixin checks if the state value differs from what was sent. If so, it sends a follow-up request with the latest value.
-
Server response: When the state finally stabilizes, the server response is applied (if provided).
Example scenario:
User rapidly clicks a like button: Like → Unlike → Like
- First click: UI shows "liked", request sent with
true - Second click: UI shows "unliked", no new request yet (one in flight)
- Third click: UI shows "liked", no new request yet
- First request completes: Mixin sees state (
true) matches what was sent (true), so no follow-up needed - UI remains "liked", server is in sync
Customization:
Override
onFinish()to run code after synchronization completes:Future<AppState?> onFinish(Object? error) async { if (error != null) { // Reload from server on error var data = await api.loadItem(itemId); return state.copyWith(items: state.items.update(itemId, data)); } return null; } -
-
Added the
OptimisticSyncWithPushandServerPushmixins.Use these mixins together when your app receives server-pushed updates (WebSockets, Server-Sent Events, Firebase, etc.) that may modify the same state your actions control.
Read the documentation in their own code to understand how they work.
Important: If your app does NOT receive server-pushed updates, you should use the simpler
OptimisticSyncmixin instead.See file
example/lib/main_optimistic_sync_with_push.dartfor an example app demonstrating the use ofOptimisticSyncWithPushin a like button.
-
-
26.1.011 Dec 2025Release notes
Open source →-
Updated website documentation in asyncredux.com.
-
Added the
Freshmixin.Suppose you want to load from the server the information needed to show a
UserProfileScreen. You can dispatch actionLoadUserProfilefrom theinitState()method of your widget:class UserProfileScreen extends StatefulWidget { _UserProfileScreenState createState() => _UserProfileScreenState(); } class _UserProfileScreenState extends State<UserProfileScreen> { void initState() { super.initState(); store.dispatch(LoadUserProfile()); // Here! } Widget build(BuildContext context) => ... }Now, add
with Freshto theLoadUserProfileaction, which loads the user profile:class LoadUserProfile extends AppAction with Fresh { Future<AppState> reduce() async { var profile = await loadUserProfile(); return state.copy(profile: profile); } }To keep the data fresh for one minute, override
freshFor, which is in milliseconds.class LoadUserProfile extends AppAction with Fresh { int freshFor = 60000; // Here! ... }Now, if the user leaves the screen and returns in less than a minute, the profile will not be loaded again, because it is still fresh. If the user returns later, the information is loaded again.
Another example
Suppose widget
UserAvatarloads its own information when mounted:class UserAvatar extends StatefulWidget { final String userId; UserAvatar(this.userId); _UserAvatarState createState() => _UserAvatarState(); } class _UserAvatarState extends State<UserAvatar> { void initState() { super.initState(); store.dispatch(LoadUserAvatar(widget.userId)); // Here! } Widget build(BuildContext context) => ... }Now add
with Freshto theLoadUserAvataraction, which loads the user avatar, and also overridefreshKeyParams()so that each different user id has its own fresh period:class LoadUserAvatar extends AppAction with Fresh { final String userId; LoadUserAvatar(this.userId); Object? freshKeyParams() => userId; // Here! Future<AppState> reduce() async { var avatar = await loadUserAvatar(userId); return state.copy(avatars: {...state.avatars, userId: avatar}); } }Now, if the avatar for a given user is shown more than once in the screen, it will only be loaded for that user once, effectively deduplicating the loading of the same avatar multiple times.
In more detail
The
Freshmixin lets you mark the result of an action as "fresh" for a set amount of time. While the result is fresh, repeated dispatches of the same action (or of other actions that share the same fresh key) are skipped because the current state already has valid data. When the fresh period ends, the result becomes "stale" and the next dispatch runs the action again.This is useful for actions that load information from a server. You can think of the fresh period as the time during which the loaded data is still good to use.
Fresh-keys
By default, the fresh-key is based on the action
runtimeTypeand the value returned byfreshKeyParams(). If you need separate fresh periods per id, url, or some other field, overridefreshKeyParams():class LoadUserCart extends AppAction with Fresh { final String userId; LoadUserCart(this.userId); // Each different `userId` in action LoadUserCart has its own fresh period. Object? freshKeyParams() => userId; ... }You can also return a tuple if you want the key to depend on more than one field:
// Each different `LoadUserCart`, `userId`, and `cartId` combination has its own fresh period. Object? freshKeyParams() => (userId, cartId);The
Freshmixin has many other useful features. See the documentation at asyncredux.com to learn aboutignoreFresh,computeFreshKey(), and more. -
Mixins now warn you when you use incompatible mixins together.
-
Now, you can simply use
dispatch(action)in widgets, instead ofcontext.dispatch(action). For example:Widget build(BuildContext context) { return ElevatedButton( onPressed: () { dispatch(MyAction()); // Here! }, child: Text('Press me'), ); }The same applies to the other dispatch extension methods like
dispatchAndWait(),dispatchAll(),dispatchAndWaitAll(), anddispatchSync().Note this works only when your app has a single StoreProvider, which is recommended and almost always true. Otherwise, you need to continue using
context.dispatch()etc.
-
-
26.0.002 Dec 2025Release notes
Open source →-
BREAKING: This version requires newer Android tooling (Android Gradle Plugin 8.12.1 or higher, Gradle 8.13 or higher, and Kotlin 2.2.0). Projects using older Android setups must update their environment before upgrading to this release. Workaround: If you want to keep using older Gradle plugins, simply add the following to the dependencies in your
pubspec.yamlfile:connectivity_plus: ^6.0.0. -
You can now use the new
MockBuildContextto test connector widgets (smart widgets) that rely onBuildContextextensions likecontext.state,context.select(),context.dispatch(), and others.This lets you test both state and callbacks without putting the widget in the widget tree (regular
testcalls, no need to usetestWidgets). For example:// Define your smart widget (connector) using context extensions. class MyConnector extends StatelessWidget { @override Widget build(BuildContext context) { return MyWidget( name: context.state.name, onChangeName: () => context.dispatch(ChangeName('Bob')), ); } } class ChangeName extends ReduxAction<AppState> { final String newName; ChangeName(this.newName); AppState reduce() => state.copy(name: newName); } // Test the connector. test('MyConnector', () { // Create a store with the desired state. var store = Store<AppState>(initialState: AppState(name: 'John')); // Create a mock context and build the widget. var context = MockBuildContext(store); var widget = MyConnector().build(context) as MyWidget; // Test the widget state. expect(widget.name, 'John'); // Test the widget callbacks. widget.onChangeName(); expect(store.state.name, 'Bob'); });Note, the dumb widget
MyWidgetis a simple widget that takesnameandonChangeName. You can test it with normal presentation tests (usingtestWidgets) without a store. Just pass the needed values to its constructor. For example:// Define your dumb widget. class MyWidget extends StatelessWidget { final String name; final VoidCallback onChangeName; const MyWidget({required this.name, required this.onChangeName}); @override Widget build(BuildContext context) { return TextButton(onPressed: onChangeName, child: Text(name)); } } // Test it. testWidgets('MyWidget', (tester) async { bool called = false; await tester.pumpWidget(MaterialApp( home: MyWidget(name: 'John', onChangeName: () => called = true), )); expect(find.text('John'), findsOneWidget); await tester.tap(find.byType(TextButton)); expect(called, true); }); -
StoreConnectoris now considered deprecated. It will not be marked as deprecated and will never be removed, but you don't need to use it for new code. For new code, when you want to implement the smart/dumb widget pattern, preferBuildContextextensions to implement the pattern, along withMockBuildContextfor testing, as shown above.The goal of
StoreConnectorwas to separate dumb widgets from smart widgets and let you test the view model without mounting it. Then you could test the dumb widget with simple presentation tests.MockBuildContextgives you the same benefits, because the dumb widget itself, when built with a mock context, works as the view model you can inspect and use to call callbacks.This makes
StoreConnectorunnecessary.MockBuildContextis simpler to use and avoids extra view model classes and factories.
-
-
25.6.330 Nov 2025Release notes
Open source →-
You can now use context extensions to dispatch actions from the
initState()anddispose()methods of aStatefulWidget.class MyScreen extends StatefulWidget { State<MyScreen> createState() => _MyScreenState(); } class _MyScreenState extends State<MyScreen> { void initState() { super.initState(); context.dispatch(LoadDataAction()); } void dispose() { context.dispatch(CleanupAction()); super.dispose(); } Widget build(BuildContext context) => Text(context.state.data); }Note: For this feature to work, your app must have a single
StoreProvider(that's usually the case).
-
-
25.6.220 Nov 2025Release notes
Open source →-
You can now use the selector extension
context.select((state) => ...)to select only the part of the state you need in your widget, so that your widget only rebuilds when that particular part of the state changes. For example:var myInfo = context.select((state) => state.myInfo);Note you can also access your state directly with
context.state.myInfo, but that will rebuild your widget whenever any part of the state changes. Usingcontext.select()is more efficient because it only rebuilds your widget when the selected part of the state changes.Suggestion: When creating the first draft of your widget, you may use
context.statejust to get started quickly, and then later change it to usecontext.select()to optimize the rebuilds.If you want to read your state and NOT rebuild your widget when the state changes, you can use
context.read(). For example:var myInfo = context.read().myInfo;However, to use
context.select(),context.read(), andcontext.stateas shown above, you need to define the following extension method in your own code (assuming your state class is calledAppState):extension BuildContextExtension on BuildContext { R select<R>(R Function(AppState state) selector) => getSelect<AppState, R>(selector); }Note, you can also use the other context extension methods like
context.dispatch,context.isWaiting,context.isFailed,context.exceptionFor,context.event,context.clearExceptionFor,context.env, and much more.See the: <a href="https://github.com/marcglasberg/async_redux/blob/master/example/lib/main_select.dart"> Select Example</a>.
-
You can now use the event extension
context.event((state) => ...)to consume events from the state. These are one-time notifications used to trigger side effects in widgets, such as showing dialogs, clearing text fields, or navigating to new screens. Unlike regular state values, events are automatically "consumed" (marked as spent) after being read, ensuring they only trigger once.First, define events in your state class and initialize them as spent:
class AppState { final Evt clearTextEvt; final Evt<String> changeTextEvt; AppState({required this.clearTextEvt, required this.changeTextEvt}); static AppState initialState() => AppState( clearTextEvt: Evt.spent(), changeTextEvt: Evt<String>.spent(), ); }Then, your actions create new events by adding them in the state:
// Boolean event. class ClearText extends AppAction { AppState reduce() => state.copy(clearTextEvt: Evt()); } // Event with a String payload. class ChangeText extends AppAction { Future<AppState> reduce() async { String newText = await fetchTextFromApi(); return state.copy(changeTextEvt: Evt<String>(newText)); } }Finally, use
context.event((state) => ...)to consume events in the build method of your widgets:bool clearText = context.event((state) => state.clearTextEvt); if (clearText) controller.clear(); String? newText = context.event((state) => state.changeTextEvt); if (newText != null) controller.text = newText;To use
context.event()as shown above, you need to define the following extension method in your own code (assuming your state class is calledAppState):extension BuildContextExtension on BuildContext { R? event<R>(Evt<R> Function(AppState state) selector) => getEvent<AppState, R>(selector); }Important notes:
- Events are consumed only once. After consumption, they are marked as " spent" and won't trigger again until a new event is dispatched.
- Each event can be consumed by only one widget. If you need multiple widgets to react to the same trigger, use separate events in the state.
- Initialize events in the state as spent:
Evt.spent()orEvt<T>.spent(). - For events with no generic type (
Evt): Returns true if the event was dispatched, or false if it was already spent. - For events with a value type (
Evt<T>): Returns the value if the event was dispatched, or null if it was already spent.
See the: <a href="https://github.com/marcglasberg/async_redux/blob/master/example/lib/main_event.dart"> Event Example</a>.
-
You can now use the environment extension
context.envto access the store "environment" for dependency injection. This environment is a container for injected services that can be accessed from both widgets and actions.First, define your environment interface and implementation:
abstract class Environment { ApiService get api; AuthService get auth; } class EnvironmentImpl implements Environment { final ApiService api = ApiServiceImpl(); final AuthService auth = AuthServiceImpl(); }Then, provide the environment when creating the store:
var store = Store<AppState>( initialState: AppState.initialState(), environment: EnvironmentImpl(), );To access the environment in your actions, extend
ReduxActionto provide typed access:abstract class Action extends ReduxAction<AppState> { Environment get env => super.env as Environment; } // Usage class LoadUserAction extends Action { Future<AppState> reduce() async { var user = await env.api.getUser(); return state.copy(user: user); } }To access the environment in your widgets, define an extension method:
extension BuildContextExtension on BuildContext { Environment get env => getEnvironment<AppState>() as Environment; }Then use it in your widgets:
Widget build(BuildContext context) { final env = context.env; // Use env.api, env.auth, etc. ... }Benefits of using the environment:
- Dependency Injection: Inject services, repositories, and other dependencies.
- Testability: Easily swap implementations for testing (mock services, test APIs, etc.).
- Clean Architecture: Keep your actions and widgets decoupled from concrete implementations.
See the: <a href="https://github.com/marcglasberg/async_redux/blob/master/example/lib/main_dependency_injection.dart"> Environment Example</a>.
-
-
25.6.119 Nov 2025Nothing published for this version
-
25.6.019 Nov 2025Nothing published for this version
-
25.5.119 Nov 2025Nothing published for this version
-
25.5.019 Nov 2025Nothing published for this version
-
25.4.023 May 2025Release notes
Open source →- Added
Store.disposeProp(key)andAction.disposeProp(key)methods to dispose and remove Futures/Timers/Streams that were previously set usingsetProp(). See Streams and Timers.
- Added
-
25.3.120 May 2025Release notes
Open source →-
In tests, you can now use
store.dispatchAndWaitAllActions. It first dispatches anaction, and then it waits until ALL current actions in progress finish dispatching. In other words, it helps make sure that the app state "settled" before you check the state.await store.dispatchAndWaitAllActions(MyAction()); -
Some internal properties used by the provided mixins are now tied to the
Storeso that they reset when the store is recreated. This is useful to make sure tests are not affected by previous tests. For example, if you dispatch an action that has some throttle, then recreate the store for another test, you can dispatch the same action again without waiting for the throttle to expire. You can also manually delete all those properties by callingstore.internalMixinProps.clear(). -
If you are running tests, you can change
store.forceInternetOnOffSimulationto simulate the internet connection as ON or OFF for the provided mixinsCheckInternet,AbortWhenNoInternet, andUnlimitedRetryCheckInternet:// There is internet store.forceInternetOnOffSimulation = () => false; // There is no internet store.forceInternetOnOffSimulation = () => false; // Uses the real internet connection status (default). store.forceInternetOnOffSimulation = () => null;
-
-
25.3.025 Apr 2025Nothing published for this version
-
25.2.025 Apr 2025Nothing published for this version
-
25.1.120 Apr 2025Release notes
Open source →-
The
Throttleaction mixin now has anignoreThrottleparameter, which allows you to ignore the throttle period for a specific action. This is useful when you want to bypass the throttle for certain actions, while still applying it to others. For example:class MyAction extends ReduxAction<AppState> with Throttle { final bool force; MyAction({this.force = false}); bool get ignoreThrottle => force; // Here! ... } -
The
Throttleaction mixin now has aremoveLockOnErrorparameter, which removes the lock when an error occurs. This is useful when you want to allow a failed action to run again within the throttle period. For example:class MyAction extends ReduxAction<AppState> with Throttle { bool removeLockOnError = true; // Here! ... } -
If your app uses AsyncRedux and your server uses Serverpod, you can add the Dart-only core package https://pub.dev/packages/async_redux_core to your server side. Now, if you throw a
UserExceptionin your backend code, that exception will automatically be thrown in the frontend. As long as the Serverpod cloud function is called inside an action, AsyncRedux will display the exception message to the user in a dialog (or other UI element that you can customize). Note: This can also be used with package i18n_extension_core to make sure the error message gets translated to the user's language. For example:UserException('The password you typed is invalid'.i18n);in the backend, will reach the frontend already translated asUserException('La contraseña que ingresaste no es válida')if the user device is in Spanish.Setup: For all this to work in Serverpod, after you import
async_redux_corein thepubspec.yamlfile of the server project, you must add theUserExceptionclass to yourgenerator.yamlfile, in itsextraClassessection:type: server ... extraClasses: - package:async_redux_core/async_redux_core.dart:UserExceptionNote: AsyncRedux also works with Celest since 22.1.0.
-
-
25.1.020 Apr 2025Nothing published for this version
-
25.1.0-dev.115 Apr 2025 pre-releaseNothing published for this version
-
25.1.0-dev.011 Apr 2025 pre-releaseNothing published for this version
-
25.0.001 Apr 2025Release notes
Open source →-
BREAKING: The action's
wrapReducemethod now returnsFutureOr<St?>instead of returningFutureOr<St?> Function()This breaking change is unlikely to affect you in any way, because thewrapReduceis an advanced feature mostly used to implement action mixins, likeRetryandDebounce. -
You can now use the
Debounceaction mixin. Debouncing delays the execution of a function until after a certain period of inactivity. Each time the debounced function is called, the period of inactivity (or wait time) is reset.The function will only execute after it stops being called for the duration of the wait time. Debouncing is useful in situations where you want to ensure that a function is not called too frequently and only runs after some “quiet time.”
For example, it’s commonly used for handling input validation in text fields, where you might not want to validate the input every time the user presses a key, but rather after they've stopped typing for a certain amount of time.
The
debouncevalue is given in milliseconds, and the default is 333 milliseconds (1/3 of a second). You can override this default:class MyAction extends ReduxAction<AppState> with Debounce { final int debounce = 1000; // Here! ... }Advanced debounce usage
The debounce is, by default, based on the action
runtimeType. This means it will reset the debounce period when another action of the same runtimeType was is dispatched within the debounce period. In other words, the runtimeType is the "lock". If you want to debounce based on a different lock, you can override thelockBuildermethod. For example, here we debounce two different actions based on the same lock:class MyAction1 extends ReduxAction<AppState> with Debounce { Object? lockBuilder() => 'myLock'; ... } class MyAction2 extends ReduxAction<AppState> with Debounce { Object? lockBuilder() => 'myLock'; ... }Another example is to debounce based on some field of the action:
class MyAction extends ReduxAction<AppState> with Debounce { final String lock; MyAction(this.lock); Object? lockBuilder() => lock; ... }See the Documentation.
-
You can now use the
Throttleaction mixin. Throttling ensures the action will be dispatched at most once in the specified throttle period. In other words, it prevents the action from running too frequently.If an action is dispatched multiple times within a throttle period, it will only execute the first time, and the others will be aborted. After the throttle period has passed, the action will be allowed to execute again, which will reset the throttle period.
If you use the action to load information, the throttle period may be considered as the time the loaded information is "fresh". After the throttle period, the information is considered "stale" and the action will be allowed to load the information again.
For example, if you are using a
StatefulWidgetthat needs to load some information, you can dispatch the loading action when widget is created, and specify a throttle period so that it doesn't load the information again too often.If you are using a
StoreConnector, you can use theonInitparameter:class MyScreenConnector extends StatelessWidget { Widget build(BuildContext context) => StoreConnector<AppState, _Vm>( vm: () => _Factory(), onInit: _onInit, // Here! builder: (context, vm) { return MyScreenConnector( information: vm.information, ... ), ); void _onInit(Store<AppState> store) { store.dispatch(LoadAction()); } }and then:
class LoadAction extends ReduxAction<AppState> with Throttle { final int throttle = 5000; Future<AppState?> reduce() async { var information = await loadInformation(); return state.copy(information: information); } }The
throttleis given in milliseconds, and the default is 1000 milliseconds (1 second). You can override this default:class MyAction extends ReduxAction<AppState> with Throttle { final int throttle = 500; // Here! ... }Advanced throttle usage
The throttle is, by default, based on the action
runtimeType. This means it will throttle an action if another action of the same runtimeType was previously dispatched within the throttle period. In other words, the runtimeType is the "lock". If you want to throttle based on a different lock, you can override thelockBuildermethod. For example, here we throttle two different actions based on the same lock:class MyAction1 extends ReduxAction<AppState> with Throttle { Object? lockBuilder() => 'myLock'; ... } class MyAction2 extends ReduxAction<AppState> with Throttle { Object? lockBuilder() => 'myLock'; ... }Another example is to throttle based on some field of the action:
class MyAction extends ReduxAction<AppState> with Throttle { final String lock; MyAction(this.lock); Object? lockBuilder() => lock; ... }See the Documentation.
-
-
25.0.0-dev.025 Feb 2025 pre-releaseNothing published for this version
-
24.2.221 Feb 2025Nothing published for this version
-
24.2.121 Feb 2025Nothing published for this version
-
24.2.021 Feb 2025Nothing published for this version
-
24.1.320 Feb 2025Nothing published for this version
-
24.1.220 Feb 2025Nothing published for this version
-
24.1.119 Feb 2025Nothing published for this version
-
24.1.019 Feb 2025Nothing published for this version
-
24.0.726 Jan 2025 -
24.0.604 Jan 2025 -
24.0.502 Jan 2025Nothing published for this version
-
24.0.401 Jan 2025Nothing published for this version
-
24.0.330 Dec 2024Nothing published for this version
-
24.0.230 Oct 2024Release notes
Open source →LocalPersistandLocalJsonPersistnow allow you to define the base directory by setting theuseBaseDirectorystatic field. The default is, as before, the application's documents directory. Other options are the cache directory (LocalPersist.useAppCacheDir), the downloads directory (LocalPersist.useAppDownloadsDir), or any other custom directory (LocalPersist.useCustomBaseDirectory).
-
24.0.130 Oct 2024Nothing published for this version
-
24.0.030 Oct 2024Nothing published for this version
-
23.3.0-dev.304 Oct 2024 pre-releaseNothing published for this version
-
23.3.0-dev.204 Oct 2024 pre-releaseNothing published for this version
-
23.3.0-dev.104 Oct 2024 pre-releaseNothing published for this version
-
23.2.007 Sep 2024Release notes
Open source →-
You can now use the
UnlimitedRetryCheckInternetto check if there is internet when you run some action that needs it. If there is no internet, the action will abort silently and then retried unlimited times, until there is internet. It will also retry if there is internet but the action failed. -
You can provide a
CloudSyncobject to the store constructor. It's similar to thePersistor, but can be used to synchronize the state of the application with the server. This is experimental. -
Fixed
isWaiting()for checking multiple actions and when state doesn't change.
-
-
23.2.0-dev.013 Aug 2024 pre-releaseNothing published for this version
-
23.1.109 Jul 2024Release notes
Open source →-
New: AsyncRedux website at https://asyncredux.com
-
New: AsyncRedux for React
-
-
23.1.009 Jul 2024Nothing published for this version
-
23.0.220 May 2024 -
23.0.113 May 2024