PackageTrack
Sign in Get early access

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 2026
2020 2021 2022 2023 2024 2025 2026
Release Pre-release Withdrawn

Releases

latest 60 of 269
  1. 28.1.0 20 Jul 2026
    Release notes
    • 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));
      
    Open source →
  2. 28.0.0 07 Jul 2026
    Release notes
    • 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
        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

      1. 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.

      2. 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)
    Open source →
  3. 28.0.0-dev.3 27 Apr 2026 pre-release

    Nothing published for this version

  4. 28.0.0-dev.2 02 Mar 2026 pre-release

    Nothing published for this version

  5. 28.0.0-dev.1 02 Mar 2026 pre-release

    Nothing published for this version

  6. 27.1.1 27 Feb 2026
    Release notes
    • 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.

    Open source →
  7. 27.1.0 27 Feb 2026

    Nothing published for this version

  8. 27.0.0 20 Feb 2026
    Release notes
    • 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.

    Open source →
  9. 26.4.2 15 Feb 2026
    Release notes
    • 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));
      

      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 ControlPricePolling action that only starts/stops the polling, and a separate FetchPrices action that does the actual fetching.

    Open source →
  10. 26.4.1 15 Feb 2026

    Nothing published for this version

  11. 26.4.0 13 Feb 2026

    Nothing published for this version

  12. 26.3.3 29 Jan 2026
    Release notes
    • Added Claude Code Skills to help developers use async_redux with AI assistants. See: https://github.com/marcglasberg/async_redux/tree/master/.claude/skills
    Open source →
  13. 26.3.2 29 Jan 2026

    Nothing published for this version

  14. 26.3.1 29 Jan 2026

    Nothing published for this version

  15. 26.3.0 29 Jan 2026

    Nothing published for this version

  16. 26.2.2 18 Jan 2026
    Release notes
    • Improved the Fresh mixin.

    • Improved mixin docs.

    Open source →
  17. 26.2.1 15 Jan 2026
    Release notes
    • Improved the OptimisticSyncWithPush mixin.
    Open source →
  18. 26.2.0 24 Dec 2025
    Release notes
    • 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();
      }
      

      Key features:

      • 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));
      }
      

      How it works:

      1. Optimistic update: When dispatched, the UI is updated immediately.

      2. 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.

      3. 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.

      4. 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

      1. First click: UI shows "liked", request sent with true
      2. Second click: UI shows "unliked", no new request yet (one in flight)
      3. Third click: UI shows "liked", no new request yet
      4. First request completes: Mixin sees state (true) matches what was sent (true), so no follow-up needed
      5. 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 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.

    Open source →
  19. 26.1.0 11 Dec 2025
    Release notes
    • 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.

      Another example

      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.

      In more detail

      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.

      Fresh-keys

      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.

    Open source →
  20. 26.0.0 02 Dec 2025
    Release notes
    • 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.

    Open source →
  21. 25.6.3 30 Nov 2025
    Release notes
    • 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).

    Open source →
  22. 25.6.2 20 Nov 2025
    Release notes
    • 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:

      • 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() or Evt<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.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:

      • 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>.

    Open source →
  23. 25.6.1 19 Nov 2025

    Nothing published for this version

  24. 25.6.0 19 Nov 2025

    Nothing published for this version

  25. 25.5.1 19 Nov 2025

    Nothing published for this version

  26. 25.5.0 19 Nov 2025

    Nothing published for this version

  27. 25.4.0 23 May 2025
    Release notes
    • Added 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.
    Open source →
  28. 25.3.1 20 May 2025
    Release notes
    • 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;
      
    Open source →
  29. 25.3.0 25 Apr 2025

    Nothing published for this version

  30. 25.2.0 25 Apr 2025

    Nothing published for this version

  31. 25.1.1 20 Apr 2025
    Release notes
    • 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.

    Open source →
  32. 25.1.0 20 Apr 2025

    Nothing published for this version

  33. 25.1.0-dev.1 15 Apr 2025 pre-release

    Nothing published for this version

  34. 25.1.0-dev.0 11 Apr 2025 pre-release

    Nothing published for this version

  35. 25.0.0 01 Apr 2025
    Release notes
    • 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!
         ...
      }
      

      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 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!
         ...
      }
      

      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 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.

    Open source →
  36. 25.0.0-dev.0 25 Feb 2025 pre-release

    Nothing published for this version

  37. 24.2.2 21 Feb 2025

    Nothing published for this version

  38. 24.2.1 21 Feb 2025

    Nothing published for this version

  39. 24.2.0 21 Feb 2025

    Nothing published for this version

  40. 24.1.3 20 Feb 2025

    Nothing published for this version

  41. 24.1.2 20 Feb 2025

    Nothing published for this version

  42. 24.1.1 19 Feb 2025

    Nothing published for this version

  43. 24.1.0 19 Feb 2025

    Nothing published for this version

  44. 24.0.7 26 Jan 2025
    Release notes
    • Added some missing params to MockStore constructor.
    Open source →
  45. 24.0.6 04 Jan 2025
    Release notes
    • Fixed translation typo bug.
    Open source →
  46. 24.0.5 02 Jan 2025

    Nothing published for this version

  47. 24.0.4 01 Jan 2025

    Nothing published for this version

  48. 24.0.3 30 Dec 2024

    Nothing published for this version

  49. 24.0.2 30 Oct 2024
    Release notes
    • 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).
    Open source →
  50. 24.0.1 30 Oct 2024

    Nothing published for this version

  51. 24.0.0 30 Oct 2024

    Nothing published for this version

  52. 23.3.0-dev.3 04 Oct 2024 pre-release

    Nothing published for this version

  53. 23.3.0-dev.2 04 Oct 2024 pre-release

    Nothing published for this version

  54. 23.3.0-dev.1 04 Oct 2024 pre-release

    Nothing published for this version

  55. 23.2.0 07 Sep 2024
    Release notes
    • You can now use the UnlimitedRetryCheckInternet to 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 CloudSync object to the store constructor. It's similar to the Persistor, 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.

    Open source →
  56. 23.2.0-dev.0 13 Aug 2024 pre-release

    Nothing published for this version

  57. 23.1.1 09 Jul 2024
    Release notes
    Open source →
  58. 23.1.0 09 Jul 2024

    Nothing published for this version

  59. 23.0.2 20 May 2024
    Release notes
    • Fixed isWaiting() when action fails.
    Open source →
  60. 23.0.1 13 May 2024
    Release notes
    • Fixed disposeProps.
    Open source →

Every package, every release, already written down.

The archive is open and free. Watching your own project is what we are building next.

Browse the archive