NewYour coding agent can read the release notes before it upgrades.Set up the MCP server →
pub.dev · #3162 most downloaded on pub.dev
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.
Last release 2 days ago
06 Oct 2026
Release timing varies
gaps range from 9 days to 2 months
Some releases are documented
notes for 30 of the last 60 stable releases
1 version withdrawn
withdrawn after publishing
7 years old
277 releases · first in 2019
Nothing published for this version
context.select and context.event now also work in didChangeDependencies. Breaking change: the debug parameter of getSelect and getEvent was removed. I…
New package async_redux_lints: an analyzer plugin that helps AI agents develop with AsyncRedux and also reports, while you type, AsyncRedux mistakes that would otherwise only fail at runtime. Most come with a quick fix. See its README for how to install it.
context.select and context.event now also work in didChangeDependencies.
Breaking change: the debug parameter of getSelect and getEvent was removed.
If you passed debug: false, just remove it.
New @stateClass annotation, that you should use to annotate all state classes like
AppState, and classes used inside the state. The async_redux_lints plugin reports
state classes that have non-final fields, that don't implement == and hasCode and
more. Prefer using @stateClass instead of @immutable for state classes, because the
linter is going to be more complete.
Fixed bugs with exceptionFor, isFailed and clearExceptionFor.
One column per quarter.
Nothing published for this version
Breaking change: Removed the following, which were already deprecated:
Breaking change: Removed the following, which were already deprecated:
ErrorObserver: Use GlobalErrorObserver instead.DevelopmentErrorObserver: Use GlobalErrorObserverForDevelopment instead.SwallowErrorObserver: Use SwallowGlobalErrorObserver instead.GlobalWrapError: Use GlobalErrorObserver instead.GlobalWrapErrorDummy: Use GlobalErrorObserver (for example,
GlobalErrorObserverDummy) instead.errorObserver parameter of Store and MockStore: Use globalErrorObserver
instead.globalWrapError parameter of Store and MockStore: Use
globalErrorObserver
instead.Store.dispatchAsync: Use dispatchAndWait instead.MockStore.dispatchAsync: Use dispatchAndWait instead.ReduxAction.dispatchAsync: Use dispatchAndWait instead.VmFactory.dispatchAsync: Use dispatchAndWait instead.DispatchAsync typedef: Use DispatchAndWait instead.VmFactory.widget: Use connector instead.Breaking change: Removed the following, which were used only for tests:
StoreTester: Use the Store (or MockStore) directly instead, with
dispatchAndWait, waitCondition, waitAllActions, waitActionType,
waitAllActionTypes and the other wait methods.TestErrorObserver: Use the Store directly instead of StoreTester.TestInfoList: Use the Store directly instead of StoreTester.StoreExceptionTimeout: Use the Store directly instead of StoreTester.StateCondition typedef: Use Store.waitCondition instead.TestInfo and the TestInfoPrinter typedef: Use ActionObserver and
StateObserver instead.testInfoPrinter parameter of Store and MockStore: Use the
actionObservers
and stateObservers parameters instead.Store.testInfoPrinter, Store.initTestInfoPrinter, Store.initTestInfoController
and Store.createTestInfoSnapshot: Use ActionObserver and StateObserver
instead.Store.onReduce: Use ActionObserver and StateObserver instead, or
Store.onChange
to listen to state changes.Breaking change: GlobalErrorObserver.action is now nullable (ReduxAction<St>?).
It's null when the error didn't come from an action, but from somewhere else, such
as from the Persistor (see below). If your observer uses the action, you now have to
check for null. For example:
class MyGlobalErrorObserver extends GlobalErrorObserver<AppState> {
@override
Object? observe() {
if (action == null) log('Persistence error: $error');
else log('Error in ${action.runtimeType}: $error');
return error;
}
}
New feature: Errors thrown by the Persistor (and by the cloudSync) when saving the
state are now handled by the store. Before, an error thrown by
Persistor.persistDifference would become an unhandled async error, which was not seen
by the GlobalErrorObserver. Now:
The error is first given to the new Persistor.wrapError method, which works the
same way as ReduxAction.wrapError: Return the error unaltered to keep it, return
another error to replace it (for example, a UserException), or return null to
swallow it. By default, it returns the error unaltered.
Then, the error is given to the GlobalErrorObserver, with a null action, so that
you can log it, or change it, or swallow it, the same way you do with action errors.
Here, error is the error after Persistor.wrapError, and originalError is the
error before it. If Persistor.wrapError returns null, the observer is not
called.
If the error is a UserException (either thrown by the persistor, or returned by
the
GlobalErrorObserver), it goes to the store's error queue, so that it can be shown
to the user, for example by the UserExceptionDialog. As usual, a UserException
with noDialog is not added to the queue.
If the GlobalErrorObserver returns null, the error is swallowed.
Any other error is still thrown as an unhandled async error, as before.
In any case, the store keeps working, and the persistor will keep persisting the state in the future.
New feature: The Persistor now has an addError method, to report errors even
before the store is created. This is useful in readState, which is usually called
when the app starts, before the store exists, so it can't show errors to the user by
throwing them. For example, if the saved data is in the format of an old version of
the app, and can't be read, you may delete it and let the user know:
Future<AppState?> readState() async {
try {
return await _read();
} on FormatException catch (error) {
await deleteState();
addError(UserException('Could not read your data, so it was reset.').addCause(error));
return null;
}
}
The errors are kept by the persistor until the store gets them with
Persistor.getAndRemoveFirstError, which happens when the store is created, and then
again after each persistence operation (reading, deleting or saving the state). Each
error is given to the GlobalErrorObserver (with a null action). Then, if the
resulting error is a UserException, it goes to the store's error queue, so that it's
shown to the user. Other errors are thrown as unhandled async errors. Note these errors
are not given to Persistor.wrapError, since you are adding them on purpose.
Breaking change: A Persistor must now be created with extends Persistor, and not
with
implements Persistor, since it now keeps its own private error queue.
Bug fix: The UserExceptionDialog now shows the errors that were already in the store's
error queue when it was mounted. Before, those errors were lost. This is important
for the errors added with Persistor.addError in readState (see above), but also for
actions that fail before the app starts.
Bug fix: The PersistorPrinterDecorator now prints the new state correctly.
Bug fix: When Persistor.persistDifference failed, the new state was still considered
persisted, and the next call to persistDifference would receive it as its
lastPersistedState. For persistors that save only the difference, the changes of the
failed call would be lost. Now, when saving fails, lastPersistedState is kept as the
last state that was actually persisted, so the next call includes the failed changes.
Bug fix: The Polling mixin now really waits for each run to finish before scheduling the next tick, as documented. Note: While the documented behavior
Bug fix: The Polling mixin now really waits for each run to finish before
scheduling the next tick, as documented. Note: While the documented behavior is
unchanged, the actual behavior is. If your code depended on ticks happening at a fixed
rate, see the new pollWaitsForRun param below.
Bug fix: Improved Poll.start, Poll.stop and Poll.runNowAndRestart.
New feature: The Polling mixin now has a pollWaitsForRun getter, to choose between
the two behaviors. It defaults to true, meaning runs never overlap, as described
above. Make it return false to get ticks at a fixed rate, measured from the start of
each run. In that case runs may overlap, so consider adding NonReentrant, Throttle
or Sequential to the action returned by createPollingAction.
class PollPrices extends AppAction with Polling {
@override
bool get pollWaitsForRun => false;
...
}
Documentation: When combining Polling with CheckInternet,
AbortWhenNoInternet, NonReentrant, Throttle, Fresh or Sequential, add
those mixins to the action returned by createPollingAction, and NOT to the
action that starts and stops the polling. All of them can abort or fail a
dispatch, and they can't tell a Poll.stop apart from a regular tick. So, on
the polling controller, they may block the Poll.stop itself, leaving you
unable to stop the polling. For example, a Poll.stop dispatched inside a
Throttle period is silently ignored, and a Poll.stop dispatched with
CheckInternet and no internet fails in before, so it never reaches
wrapReduce.
New Sequential mixin. Actions that use it enter a shared FIFO queue and run one at a time, in the exact order they were dispatched, across all partici
New Sequential mixin. Actions that use it enter a shared FIFO queue and run
one at a time, in the exact order they were dispatched, across all participating action
types. The queue position is reserved synchronously at dispatch time, and the next
action is released when the previous one completes, fails, or is aborted. Override
sequentialKeyParams to have independent queues (the default key is null, a
single global queue), and override boolean discardQueueOnError to abort the queue
when an action fails:
class SaveItem extends ReduxAction<AppState> with Sequential {
Future<AppState?> reduce() async { ... }
}
Important: an action that holds the queue must not await dispatchAndWait(...) another
action of the same queue, because that would deadlock. Use dispatch(...) without
awaiting instead.
The Sequential mixin can be combined with CheckInternet, NoDialog,
AbortWhenNoInternet, NonReentrant, Retry, UnlimitedRetries, Throttle, Fresh
and OptimisticCommand. It can also be combined with Polling, but preferably on the
action returned by createPollingAction, and not on the action that starts and stops
the polling.
It cannot be combined with Debounce, OptimisticSync, OptimisticSyncWithPush or
ServerPush, which all need the action (or its optimistic state change) to happen as
soon as it is dispatched, and not when it gets its turn in the queue. It also cannot be
combined with UnlimitedRetryCheckInternet, which drops a dispatch while another action
of the same type is in progress; since a queued action does count as being in progress,
actions of the same type would be silently dropped instead of being ordered. All these
combinations now throw an assertion error in debug mode. See the Sequential
documentation, and mixin_compatibility.md, for the details.
You can now use waitActionType and waitAllActionTypes inside the reducer of an
action, to wait for actions of the given types that may be running right now, before
continuing. If no action of the given types is running, they complete at once.
// If a LoadUserProfile is running, wait for it to finish. Otherwise, continue at once.
await waitActionType(LoadUserProfile);
// Same, but wait for all LoadUserProfile and LoadUserSettings actions.
await waitAllActionTypes([LoadUserProfile, LoadUserSettings]);
IMPORTANT: These methods are meant for one-directional dependencies, such as waiting for
data that another action may be loading right now. They are NOT locks or queues, and
must NOT be used to make actions run one at a time. Use the Sequential mixin for that.
Circular waits deadlock: if ActionA waits for ActionB while ActionB waits for
ActionA, both hang forever. Only wait for actions that never wait for you, directly or
indirectly. The docs of these methods lists all the cases to avoid.
Waiting for the type of the current action itself would also deadlock, so
waitActionType throws a StoreException if given the current action's own type, while
waitAllActionTypes simply ignores it. Likewise, waitAllActions now ignores the
current action instance if present in its list.
ReduxAction.waitAllActions no longer times out (it used to fail with a
TimeoutException after 10 minutes). Like waitActionType and waitAllActionTypes
above, it now waits for as long as it takes for the given actions to finish. Its
completeImmediately parameter is unchanged, and the Store.waitAllActions method used
in tests is also unchanged.
Nothing published for this version
Nothing published for this version
New extension methods thenIfCompletedOk and thenIfCompletedFailed on Future , which is the type returned by dispatchAndWait. They let you chain code t
New extension methods thenIfCompletedOk and thenIfCompletedFailed
on Future<ActionStatus>, which is the type returned by dispatchAndWait.
They let you chain code that should run only if the action completed OK,
or only if it failed. This is important because dispatchAndWait completes
with an ActionStatus even 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));
DEPRECATION WARNING: Store.globalWrapError and Store.errorObserver are now deprecated. Use the new globalErrorObserver instead.
DEPRECATION WARNING: Store.globalWrapError and Store.errorObserver
are now deprecated. Use the new globalErrorObserver instead.
You can now provide a global error observer using the globalWrapError parameter in
the Store constructor:
var store = Store<AppState>(
initialState: AppState(),
globalErrorObserver: (store) => MyGlobalErrorObserver(),
);
class MyGlobalErrorObserver extends GlobalErrorObserver {
@override
Object? observe() {
// ...
}
}
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 some PlatformExceptions 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
a UserException. Note, this could also be done in the ReduxAction.wrapError,
but then you'd have to add it to all actions that use Firebase.
BREAKING: Removed the deprecated Store.wrapError.
Use the new globalErrorObserver instead.
BREAKING: Removed the deprecated:
ActionStatus.isBeforeDone (replace with hasFinishedMethodBefore)isReduceDone (replace with hasFinishedMethodReduce)isAfterDone (replace with hasFinishedMethodAfter)isFinished (replace with isBeforeDone && isReduceDone && isAfterDone)Nothing published for this version
Nothing published for this version
Nothing published for this version
Added store.removeError(source) to remove UserException errors from the error queue. You can pass it a UserException, an ActionStatus, or a ReduxActio
Added store.removeError(source) to remove UserException errors from the error queue.
You can pass it a UserException, an ActionStatus, or a ReduxAction.
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.context now has a reference to the action and the store.
Nothing published for this version
BREAKING: This version is only a breaking change if you are using the enviroment parameter of the Store constructor to do dependency injection.
BREAKING: This version is only a breaking change if you are using the enviroment
parameter of the Store constructor to do dependency injection.
The Store constructor now accepts dependencies and configuration parameters,
in addition to environment. See file main_dependency_injection.dart in the example
directory 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 the Store,
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.env has been renamed to Store.environment.
BREAKING: Removed ReduxAction.env. Access it through store.environment instead.
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 through store.dependencies
instead. 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.
Added the Polling mixin and Poll enum.
Added the Polling mixin and Poll enum.
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 Poll enum: Poll.start to begin polling (also runs the
action immediately), Poll.stop to cancel it,
Poll.runNowAndRestart to run immediately and restart the timer, and Poll.once
to 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));
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 ControlPricePolling action that only
starts/stops the polling, and a separate FetchPrices action that does the actual
fetching.
Nothing published for this version
Nothing published for this version
Added Claude Code Skills to help developers use async_redux with AI assistants. See: https://github.com/marcglasberg/async_redux/tree/master/.claude/s
async_redux with AI assistants.
See: https://github.com/marcglasberg/async_redux/tree/master/.claude/skillsNothing published for this version
Nothing published for this version
Nothing published for this version
* Improved the Fresh mixin. * Improved mixin docs.
Improved the Fresh mixin.
Improved mixin docs.
Improved the OptimisticSyncWithPush mixin.
OptimisticSyncWithPush mixin.Added the OptimisticCommand mixin.
Added the OptimisticCommand mixin.
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.dart for an example app
demonstrating the use of OptimisticCommand in 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();
}
Instant UI update: The state is updated immediately when the action is dispatched, before the server request completes.
Automatic rollback: If sendCommandToServer fails, 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 OptimisticSync mixin.
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.dart for an example app
demonstrating the use of OptimisticSync in 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));
}
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).
User rapidly clicks a like button: Like → Unlike → Like
truetrue) matches what was
sent (true), so no follow-up neededOverride 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 OptimisticSyncWithPush and ServerPush mixins.
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 OptimisticSync mixin instead.
See file example/lib/main_optimistic_sync_with_push.dart for an example app
demonstrating the use of OptimisticSyncWithPush in a like button.
Updated website documentation in asyncredux.com.
Updated website documentation in asyncredux.com.
Added the Fresh mixin.
Suppose you want to load from the server the information needed to show a
UserProfileScreen. You can dispatch action LoadUserProfile from the
initState() 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 Fresh to the LoadUserProfile action, 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.
Suppose widget UserAvatar loads 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 Fresh to the LoadUserAvatar action, which loads the user
avatar, and also override freshKeyParams() 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.
The Fresh mixin 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.
By default, the fresh-key is based on the action runtimeType and the value
returned by freshKeyParams(). If you need separate fresh periods per id,
url, or some other field, override freshKeyParams():
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 Fresh mixin has many other useful features. See the documentation at
asyncredux.com to learn about ignoreFresh,
computeFreshKey(), and more.
Mixins now warn you when you use incompatible mixins together.
Now, you can simply use dispatch(action) in widgets, instead of
context.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(),
and dispatchSync().
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.
StoreConnector is 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.…
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.yaml file:
connectivity_plus: ^6.0.0.
You can now use the new MockBuildContext to test connector widgets
(smart widgets) that rely on BuildContext extensions like context.state,
context.select(), context.dispatch(), and others.
This lets you test both state and callbacks without putting the widget in the
widget tree (regular test calls, no need to use testWidgets).
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 MyWidget is a simple widget that takes name and
onChangeName. You can test it with normal presentation tests (using
testWidgets) 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);
});
StoreConnector is 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,
prefer BuildContext extensions to implement the pattern,
along with MockBuildContext for testing, as shown above.
The goal of StoreConnector was 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.
MockBuildContext gives 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 StoreConnector unnecessary. MockBuildContext is simpler to use
and avoids extra view model classes and factories.
You can now use context extensions to dispatch actions from the initState() and dispose() methods of a StatefulWidget.
You can now use context extensions to dispatch actions from the initState()
and dispose() methods of a StatefulWidget.
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).
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 widge
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.
Using context.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.state just to get started quickly, and then later change it
to use context.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(), and context.state
as shown above, you need to define the following extension method in your
own code (assuming your state class is called AppState):
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 called
AppState):
extension BuildContextExtension on BuildContext {
R? event<R>(Evt<R> Function(AppState state) selector) => getEvent<AppState, R>(selector);
}
Important notes:
Evt.spent() or
Evt<T>.spent().Evt): Returns true if the event
was dispatched, or false if it was already spent.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.env to 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 ReduxAction to 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:
See the: <a href="https://github.com/marcglasberg/async_redux/blob/master/example/lib/main_dependency_injection.dart"> Environment Example</a>.
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Added Store.disposeProp(key) and Action.disposeProp(key) methods to dispose and remove Futures/Timers/Streams that were previously set using setProp()
Store.disposeProp(key) and Action.disposeProp(key) methods to
dispose and remove Futures/Timers/Streams that were previously set
using setProp().
See Streams and Timers.In tests, you can now use store.dispatchAndWaitAllActions. It first dispatches an action, and then it waits until ALL current actions in progress fini
In tests, you can now use store.dispatchAndWaitAllActions. It first
dispatches an action, 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
Store so 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 calling
store.internalMixinProps.clear().
If you are running tests, you can change store.forceInternetOnOffSimulation
to simulate the internet connection as ON or OFF for the provided mixins
CheckInternet, AbortWhenNoInternet, and UnlimitedRetryCheckInternet:
// There is internet
store.forceInternetOnOffSimulation = () => false;
// There is no internet
store.forceInternetOnOffSimulation = () => false;
// Uses the real internet connection status (default).
store.forceInternetOnOffSimulation = () => null;
Nothing published for this version
Nothing published for this version
The Throttle action mixin now has an ignoreThrottle parameter, which allows you to ignore the throttle period for a specific action. This is useful wh
The Throttle action mixin now has an ignoreThrottle parameter, 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 Throttle action mixin now has a removeLockOnError parameter, 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 UserException in 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 as
UserException('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_core
in the pubspec.yaml file of the server project, you must add the
UserException class to your generator.yaml file, in its extraClasses
section:
type: server
...
extraClasses:
- package:async_redux_core/async_redux_core.dart:UserException
Note: AsyncRedux also works with Celest since 22.1.0.
Nothing published for this version
Nothing published for this version
Nothing published for this version
BREAKING: The action's wrapReduce method now returns FutureOr instead of returning FutureOr Function() This breaking change is unlikely to affect you…
BREAKING: The action's wrapReduce method now returns FutureOr<St?>
instead of returning FutureOr<St?> Function()
This breaking change is unlikely to affect you in any way, because the
wrapReduce is an advanced feature mostly used to implement action mixins,
like Retry and Debounce.
You can now use the Debounce action 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 debounce value 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!
...
}
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 the lockBuilder method. 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 Throttle action 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 StatefulWidget that 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 the onInit parameter:
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 throttle is 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!
...
}
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 the lockBuilder method. 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.
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Added some missing params to MockStore constructor.
MockStore constructor.* Fixed translation typo bug.
Nothing published for this version
Nothing published for this version
Nothing published for this version
LocalPersist and LocalJsonPersist now allow you to define the base directory by setting the useBaseDirectory static field. The default is, as before,
LocalPersist and LocalJsonPersist now allow you to define the base
directory by
setting the useBaseDirectory static 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).Nothing published for this version
Nothing published for this version
Nothing published for this version
Your coding agent can read these notes before it upgrades. Set up the MCP server →