Async Redux: Flutter’s non-boilerplate version of Redux

Marcelo Glasberg
Aug 5, 2019 · 7 min read

AsyncRedux is a special version of Redux which:

  1. Is easy to learn
  2. Is easy to use
  3. Is easy to test
  4. Has no boilerplate

AsyncRedux is currently used in a large-scale project I’m involved in, and I finally took the time to write its documentation and publish it. I’ll assume you already know Redux and Flutter, and need to quickly understand some of what AsyncRedux has to offer. If you want all of the details and features, please go to the AsyncRedux Flutter Package.

The most obvious feature of AsyncRedux is that there is no middleware, since reducers can be both sync and async. But let’s start from the beginning.

Declare your store and state, like this:

If you want to change the store state you must “dispatch” some action. In AsyncRedux all actions extend ReduxAction. You dispatch actions as usual:

The reducer of an action is simply a method of the action itself, called reduce. All actions must override this method.

Sync Reducer

If you want to do some synchronous work, simply declare the reducer to return AppState, then change the state and return it. For example, let’s start with a simple action to increment a counter by some value:

Simply dispatching the action above is enough to run the reducer and change the state. Unlike other Redux versions, there is no need to list middleware functions during the store’s initialization, or manually wire the reducers.

Try running the: Sync Reducer Example.

Async Reducer

If you want to do some asynchronous work, you simply declare the action’reducer to return Future<AppState> then change the state and return it. There is no need of any "middleware", like for other Redux versions.

As an example, suppose you want to increment a counter by a value you get from the database. The database access is async, so you must use an async reducer:

Try running the: Async Reducer Example.

Changing state is optional

For both sync and async reducers, returning a new state is optional. You may return null, which is the same as returning the state unchanged.

This is useful because some actions may simply start other async processes, or dispatch other actions. For example:

Before and After the Reducer

Sometimes, while an async reducer is running, you want to prevent the user from touching the screen. Also, sometimes you want to check preconditions like the presence of an internet connection, and don’t run the reducer if those preconditions are not met.

To help you with these use cases, you may override methods ReduxAction.before() and ReduxAction.after(), which run respectively before and after the reducer.

If before throws an error, then reduce will NOT run. This means you can use it to check any preconditions and throw an error if you want to prevent the reducer from running. This method is also capable of dispatching actions, so it can be used to turn on a modal barrier:

The after() method runs after reduce, even if an error was thrown by before or reduce (akin to a "finally" block), so it can be used to do stuff like turning off a modal barrier when the reducer ends, even if there was some error in the process:

This is a complete example action:

Try running the: Before and After the Reducer Example.

You may also provide reusable abstract classes with default before and after methods. For example, any action which extends the BarrierAction class below will display a modal barrier while it runs:

The above BarrierAction is demonstrated in this example.


Processing errors thrown by Actions

Suppose a logout action that checks if there is internet connection, and then deletes the database and sets the store to its initial state:

In the above code, the checkInternetConnection() function checks if there is an internet connection, and if there isn't it throws an error:

All errors thrown by action reducers are sent to the ErrorObserver, which you may define during store creation. For example:

If your error observer returns true, the error will be rethrown after the ErrorObserver finishes. If it returns false, the error is considered dealt with, and will be "swallowed" (not rethrown).

User exceptions

To show error messages to the user, make your actions throw an UserException . The special UserException error class represents “user errors” which are meant as warnings to the user, and not as “code errors”. Then wrap your home-page with UserExceptionDialog, below StoreProvider and MaterialApp:

The above code will make sure all user exceptions are shown in a dialog to the user. Try running the: Show Error Dialog Example.


Testing

AsyncRedux provides the StoreTester class that makes it easy to test both sync and async reducers. Start by creating the store-tester:

Then, dispatch some action, wait for it to finish, and check the resulting state:

The variable info above will contain information about after the action reducer finishes executing, no matter if the reducer is sync or async.

While the above example demonstrates the testing of a simple action, real-world apps have actions that dispatch other actions, sync and async. You may use the many different StoreTester methods to expect any number of expected actions, check their order, their end state, or each intermediary state between actions. For example:

Try running the: Store Tester Example .


Route Navigation

AsyncRedux comes with a NavigateAction which you can dispatch to navigate your Flutter app:

For this to work, during app initialization you must statically inject your navigator key into the NavigateAction:

Try running the: Navigate Example.


Events

In a real Flutter app it’s not practical to assume that a Redux store can hold all of the application state. Widgets like TextField and ListView make use of controllers, which hold state, and the store must be able to work alongside these. For example, in response to the dispatching of some action you may want to clear a text-field, or you may want to scroll a list-view to the top.

AsyncRedux solves these problems by introducing the concept of “events”:

Actions may use events as state:

And events may be passed down by the StoreConnector to some StatefulWidget, just like any other state:

Your widget will “consume” the event in its didUpdateWidget method, and do something with the event payload. So, for example, if you use a controller to hold the text in a TextField:

Try running the: Event Example.


This was just a quick look into AsyncRedux. The package documentation has more details, and shows many other features not mentioned here.

Este artigo tem uma versão em Português.

The AsyncRedux code is based upon packages redux and flutter_redux by Brian Egan and John Ryan. Also uses code from package equatable by Felix Angelov. Special thanks: Eduardo Yamauchi and Hugo Passos helped me with the async code, checking the documentation, testing everything and making suggestions. This work started after Thomas Burkhart explained to me why he didn’t like Redux. Reducers as methods of action classes were shown to me by Scott Stoll and Simon Lightfoot.

Other Flutter packages I’ve authored:

https://github.com/marcglasberg

https://twitter.com/GlasbergMarcelo

https://stackoverflow.com/users/3411681/marcg

Flutter Community

Articles and Stories from the Flutter Community

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch

Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore

Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store