Async Redux: Flutter’s non-boilerplate version of Redux
Tags: flutter, redux, statemanagement, architecture
Published: 2019-09-04 15:51:01

---
title: Async Redux: Flutter’s non-boilerplate version of Redux
published: true
tags: flutter,redux,state-management,software-architecture
cover_image: https://cdn-images-1.medium.com/max/969/1*O34Rwgxfa3NiE7F_QW683Q.png
---

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

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](https://pub.dev/packages/async_redux).

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:

```dart
var state = AppState.initialState();

var store = Store<AppState>(
 initialState: state, 
);
```

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

```dart
store.dispatch(IncrementAction(amount: 3));
store.dispatch(QueryAndIncrementAction());
```

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:

```dart
class IncrementAction extends ReduxAction<AppState> {
 final int amount;
 IncrementAction({this.amount}) : assert(amount != null);

@override
 AppState reduce() {
 return state.copy(counter: state.counter + amount));
 }
}
```

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](https://github.com/marcglasberg/async_redux/blob/master/example/main.dart).

#### 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:

```dart
class QueryAndIncrementAction extends ReduxAction<AppState> {

@override
 Future<AppState> reduce() async {
 int value = await getAmount();
 return state.copy(counter: state.counter + value));
 }
}
```

Try running the: [Async Reducer Example](https://github.com/marcglasberg/async_redux/blob/master/example/main_increment_async.dart).

![](https://cdn-images-1.medium.com/max/969/1*O34Rwgxfa3NiE7F_QW683Q.png)

#### 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:

```dart
class QueryAction extends ReduxAction<AppState> { 
 @override
 Future<AppState> reduce() async {
 dispatch(IncrementAction(amount: await getAmount()));
 return null;
 }
}
```

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

```dart
Future<void> before() async => await checkInternetConnection();
```

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:

```dart
void before() => dispatch(WaitAction(true));
```

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:

```dart
void after() => dispatch(WaitAction(false));
```

This is a complete example action:

```dart
// Increment a counter by 1, and then get some description text.
class IncrementAndGetDescriptionAction extends ReduxAction\<AppState\> {

 @override
 Future<AppState> reduce() async { 
 dispatch(IncrementAction());
 String description = await read("http://numbersapi.com/${state.counter}"); 
 return state.copy(description: description);
 } 

 void before() => dispatch(WaitAction(true)); 

 void after() => dispatch(WaitAction(false));
}
```

Try running the: [Before and After the Reducer Example](https://github.com/marcglasberg/async_redux/blob/master/example/main_before_and_after.dart).

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:

```dart
abstract class BarrierAction extends ReduxAction\<AppState\> {
 void before() => dispatch(WaitAction(true));
 void after() => dispatch(WaitAction(false));
}

class IncrementAndGetDescriptionAction extends BarrierAction { 
 @override
 Future<AppState> reduce() async { ... } 
}
```

The above BarrierAction is demonstrated in [this example](https://github.com/marcglasberg/async_redux/blob/master/example/main_event_redux.dart).

### 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:

```dart
class LogoutAction extends ReduxAction<AppState> { 
 @override
 Future<AppState> reduce() async { 
 await checkInternetConnection(); 
 await deleteDatabase(); 
 return AppState.initialState();
 }
}
```

In the above code, the `checkInternetConnection()` function checks if there is an [internet connection](https://pub.dev/packages/connectivity), and if there isn't it throws an error:

```dart
Future<void> checkInternetConnection() async {
 if (await Connectivity().checkConnectivity() == ConnectivityResult.none) 
 throw NoInternetConnectionException();
}
```

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

```dart
var store = Store<AppState>(
 initialState: AppState.initialState(), 
 errorObserver: errorObserver, 
);

bool errorObserver(Object error, ReduxAction action, Store store, Object state, int dispatchCount) {
 print("Error thrown during $action: $error); 
 return true;
}
```

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`:

```dart
class MyApp extends StatelessWidget {
 @override
 Widget build(BuildContext context) 
 => StoreProvider<AppState>(
 store: store,
 child: MaterialApp(
 home: UserExceptionDialog<AppState>(
 child: MyHomePage(),
 )));
}
```

The above code will make sure all user exceptions are shown in a dialog to the user. Try running the: [Show Error Dialog Example](https://github.com/marcglasberg/async_redux/blob/master/example/main_show_error_dialog.dart).

### Testing

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

```dart
var storeTester 
 = StoreTester<AppState>(initialState: AppState.initialState());
```

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

```dart
storeTester.dispatch(SaveNameAction("Mark"));
TestInfo<AppState> info = await storeTester.wait(SaveNameAction);
expect(info.state.name, "Mark");
```

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:

```dart
TestInfoList<AppState> infos = await storeTester.waitAll([
 IncrementAndGetDescriptionAction,
 WaitAction,
 IncrementAction,
 WaitAction,
]);

var info = infos[IncrementAndGetDescriptionAction];
expect(info.state.waiting, false);
expect(info.state.description, isNotEmpty);
expect(info.state.counter, 1);
```

Try running the: [Store Tester Example](https://github.com/marcglasberg/async_redux/blob/master/example/main_before_and_after_STATE_test.dart) .

### Route Navigation

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

```dart
dispatch(NavigateAction.pop()); 
dispatch(NavigateAction.pushNamed("myRoute")); 
dispatch(NavigateAction.pushReplacementNamed("myRoute")); 
dispatch(NavigateAction.pushNamedAndRemoveAll("myRoute")); 
dispatch(NavigateAction.popUntil("myRoute"));
```

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

```dart
**final** navigatorKey = GlobalKey<NavigatorState>();

**void** main() **async** { 
 NavigateAction.setNavigatorKey(navigatorKey);
 ...
}
```

Try running the: [Navigate Example](https://github.com/marcglasberg/async_redux/blob/master/example/main_navigate.dart).

### 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”:

```dart
var clearTextEvt = Event(); 
var changeTextEvt = Event<String>("Hello"); 
var myEvt = Event<int>(42);
```

Actions may use events as state:

```dart
class ClearTextAction extends ReduxAction<AppState> { 
 AppState reduce() => state.copy(clearTextEvt: Event()); 
}
```

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

```dart
class MyConnector extends StatelessWidget { 
 Widget build(BuildContext context) {
 return StoreConnector<AppState, ViewModel>(
 model: ViewModel(),
 builder: (BuildContext context, ViewModel vm) =\> MyWidget(
 initialText: vm.initialText,
 clearTextEvt: vm.clearTextEvt, 
 onClear: vm.onClear,
 ));
 }
}
```

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`:

```dart
@override
void didUpdateWidget(MyWidget oldWidget) {
 super.didUpdateWidget(oldWidget);
 consumeEvents();
}

void consumeEvents() { 
 if (widget.clearTextEvt.consume())
 WidgetsBinding.instance.addPostFrameCallback((_) {
 if (mounted) controller.clear();
 }); 
 }
```

Try running the: [Event Example](https://github.com/marcglasberg/async_redux/blob/master/example/main_event_redux.dart).

This was just a quick look into **AsyncRedux**. The [package documentation](https://pub.dev/packages/async_redux) has more details, and shows many other features not mentioned here.

_This article has a_ [_Brazilian Portuguese version_](https://medium.com/@marcglasberg/async-redux-pt-brasil-e783ceb13c43)_. English is not my mother language, so please let me know about any grammar corrections or typos._

_The AsyncRedux code is based upon packages_ [_redux_](https://pub.dev/packages/redux) _and_ [_flutter\_redux_](https://pub.dev/packages/flutter_redux) _by Brian Egan and John Ryan. Also uses code from package_ [_equatable_](https://pub.dev/packages/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_](https://github.com/escamoteur) _explained to me why he didn’t like Redux. Reducers as methods of action classes were shown to me by_ [_Scott Stoll_](https://github.com/ScottS2017) _and_ [_Simon Lightfoot_](https://github.com/slightfoot)_._

[_https://github.com/marcglasberg_](https://github.com/marcglasberg)  
[_https://twitter.com/marcglasberg_](https://twitter.com/marcglasberg)  
[_https://stackoverflow.com/users/3411681/marcg_](https://stackoverflow.com/users/3411681/marcg)

[Flutter Community](https://www.twitter.com/FlutterComm)

* * *