In the previous part of this article we went through the basics of Redux usage in Flutter by building a simple app that managed its state in a Redux store. In this part we're gonna build on top of the sample app from the first part which you can find here:

Article: Part 1 - Redux setup and basic state management

Source code: github.com/hybridheroes/flutter_redux_example_app

#Redux Thunk

Usually one of first topics that comes up when getting started with Redux is the question of how to handle side effects such as async calls and conditional flows in our state management. Surely we don't want to do that in our widgets. Nor can we use reducers for that since they're pure functions that are not allowed to have any side effects. The only place remaining is in actions and this part is all about that.

Until now our actions were simple objects created by an action class. We can have another kind of action creators which return a function instead of creating objects. These kind of action creators are called thunk actions and the function that is returned by them can contain the side effects that we need. Handling this special kinf of actions needs a middleware called "Redux Thunk" middleware. This middleware receives the returned functions from the thunk actions and executes them. Let's see all that in the code:

#Redux Thunk middleware

Redux thunk middleware is a package that we need to add to our app separately:

dependencies: redux_thunk: ^0.2.1

Now we can add the middleware to the store:

import ... import 'package:redux_thunk/redux_thunk.dart' ; void main() { ... final store = Store<AppState>( appReducer, initialState: initialState, middleware: [ thunkMiddleware, new LoggingMiddleware.printer(), ], ); ... }

#Asynchronous actions

One of the most common async calls example is the http request where we await a response from an API for example and we dispatch an action with a payload build from the response. Let's demonstrate that by creating a mock API client.

import 'dart:math' ; import 'dart:convert' ; import 'package:sample_flutter_redux_app/models/models.dart' ; class ApiClient { static Future<MyBoxColor> getBoxColor() async { Random rng = new Random(); var json = jsonEncode({ 'red' : (rng.nextDouble() * 101 ).floorToDouble() / 10 , 'green' : (rng.nextDouble() * 101 ).floorToDouble() / 10 , 'blue' : (rng.nextDouble() * 101 ).floorToDouble() / 10 , }); await Future.delayed( Duration (milliseconds: 500 )); return MyBoxColor.fromJson(jsonDecode(json)); } }

Our API client doesn't make any http requests but it creates a JSON object with random values for different colors which are used to create a MyBoxColor instance.

At this point the IDE should complain that MyBoxColor has no fromJson constructor. fromJson constructor is used as a method of deserializing JSON data in our model. To read more about JSON and serialization in Flutter here is the official documentaion:

JSON and serialization

Now we can add the thunk action to our actions file:

import 'package:redux/redux.dart' ; import 'package:redux_thunk/redux_thunk.dart' ; import 'package:sample_flutter_redux_app/models/api_client.dart' ; import 'package:sample_flutter_redux_app/models/models.dart' ; ThunkAction<AppState> getBoxColor() { return (Store<AppState> store) async { MyBoxColor boxColor = await ApiClient.getBoxColor(); store.dispatch(SetColor(boxColor)); }; } ...

As you can see our thunk action returns a function that receives the store. The body of the function calls the method from our API client and when it has the response it dispatches one of our normal actions that we already had.

Let's call this action in a widget

import 'package:flutter/material.dart' ; import 'package:redux/redux.dart' ; import 'package:flutter_redux/flutter_redux.dart' ; import 'package:sample_flutter_redux_app/actions/actions.dart' ; import 'package:sample_flutter_redux_app/models/models.dart' ; class Randomizer extends StatelessWidget { Widget build(BuildContext context) { return StoreConnector<AppState, _ViewModel>( converter: (Store<AppState> store) => _ViewModel.fromStore(store), builder: (BuildContext context, _ViewModel vm) { return RaisedButton( child: Text( 'Randomize' ), onPressed: () { vm.randomize(); }, ); }, ); } } class _ViewModel { final VoidCallback randomize; _ViewModel({ this .randomize}); static _ViewModel fromStore(Store<AppState> store) { return _ViewModel( randomize: () { store.dispatch(getBoxColor()); }, ); } }

And add the widget to the widget tree:

class MyApp extends StatelessWidget { Widget build(BuildContext context) { ... Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: <Widget>[ Text( 'Color Controller' ), Randomizer(), ], ), ... } }

Now you should be able to get random colors in the color box by pressing the randomize button. You should also notice the delay between pressing the button and actual color change which is our simulated async call. If you want to check the result in the source code checkout the branch part2_1:

git checkout part2_1

#Handling exceptions in thunk actions

We are able to dispatch async actions now and most of async flows need exception handling. Our mock API client won't throw any exception right now so let's force it to do that sometimes. Let's say if one of the values is less than 1.0 we want to throw an exception (no reasons why!). This is our new API client:

import 'dart:math' ; import 'dart:convert' ; import 'package:flutter/material.dart' ; import 'package:sample_flutter_redux_app/models/models.dart' ; import 'package:sample_flutter_redux_app/selectors/selectors.dart' ; class ColorException extends FormatException { final Color badColor; ColorException( String message, this .badColor) : super (message); } class ApiClient { static Future<MyBoxColor> getBoxColor() async { Random rng = new Random(); var json = jsonEncode({ 'red' : (rng.nextDouble() * 101 ).floorToDouble() / 10 , 'green' : (rng.nextDouble() * 101 ).floorToDouble() / 10 , 'blue' : (rng.nextDouble() * 101 ).floorToDouble() / 10 , }); await Future.delayed( Duration (milliseconds: 500 )); var color = jsonDecode(json); if (color[ 'red' ] < 1.0 || color[ 'green' ] < 1.0 || color[ 'blue' ] < 1.0 ) { throw new ColorException( 'This might not be a good color $color' , colorSelector(MyBoxColor.fromJson(color)), ); } return MyBoxColor.fromJson(color); } }

Now we can catch the ColorException is our thunk action.

ThunkAction<AppState> getBoxColor() { return (Store<AppState> store) async { try { MyBoxColor boxColor = await ApiClient.getBoxColor(); store.dispatch(SetColor(boxColor)); } on ColorException catch (e) { } }; }

Here we can simply dispatch an action that is meant for the case of exception. Or we can do something more complicated and try to open a dialog inside the widget when the exception is thrown.

In JavaScript implemetations of Redux and Thunk middleware, dispatching async actions returns a promise and you can simply wait for the promise to resolve or reject where you dispatch an action. But the current implementations in Dart, at the time of writing this, don't return a Future and you can't do the common exception handling with Futures when you dispatch a thunk action. But there is a workaround for that which is using Dart Completer from dart:async package. From the documentation of Completer class, a completer is "A way to produce Future objects and to complete them later with a value or error." It means we can pass a completer around and tell it what to do whenever we want. Let's try that:

import 'dart:async' ; import 'package:redux/redux.dart' ; import 'package:redux_thunk/redux_thunk.dart' ; import 'package:sample_flutter_redux_app/models/api_client.dart' ; import 'package:sample_flutter_redux_app/models/models.dart' ; ThunkAction<AppState> getBoxColor(Completer completer) { return (Store<AppState> store) async { try { MyBoxColor boxColor = await ApiClient.getBoxColor(); store.dispatch(SetColor(boxColor)); completer.complete(); } on ColorException catch (e) { completer.completeError(e); } }; }

We update the ViewModel of randomizer widget to add an error callback:

class _ViewModel { final Function ( Function (ColorException)) randomize; _ViewModel({ this .randomize}); static _ViewModel fromStore(Store<AppState> store) { return _ViewModel( randomize: ( Function (ColorException) onError) async { Completer completer = new Completer(); store.dispatch(getBoxColor(completer)); try { await completer.future; } on ColorException catch (e) { onError(e); } }, ); } }

And now we can decide what to do in the widget by passing an error callback when calling randomize. Here we show an alert dialog.

... import 'dart:async' ; import 'package:flutter/cupertino.dart' ; import 'package:sample_flutter_redux_app/models/api_client.dart' ; class Randomizer extends StatelessWidget { ... return RaisedButton( child: Text( 'Randomize' ), onPressed: () { vm.randomize((ColorException e) { showCupertinoDialog( context: context, builder: (context) => CupertinoAlertDialog( content: Text( e.message, style: TextStyle(color: e.badColor), ), actions: <Widget>[ FlatButton( child: Text( 'Ok' ), onPressed: () { Navigator.pop(context); }, ), ], ), ); }); }, ); ...

Checkout the final result on the branch part2_2:

git checkout part2_2

#Wrapping up

We saw how to implement async flows and exception handling in thunk actions. You can also have conditional flows in your thunk actions, group multiple actions which are meant to be dispatched together or contain complex logic in one place by using thunk actions.

I suggest you apply what we discussed in this article and define some thunk actions for the other two boxes. Hope you enjoyed.

Photo by Michael Drexler on Unsplash