02.09.2019 in entwicklung

Using Redux in Flutter

Part 2 - Thunk actions

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:

# pubspec.yaml
dependencies:
  # other dependencies...
  redux_thunk: ^0.2.1 # Use the latest version

Now we can add the middleware to the store:

// lib/main.dart
import ...
import 'package:redux_thunk/redux_thunk.dart'; // Import package

void main() {
  ...

  final store = Store<AppState>(
    appReducer,
    initialState: initialState,
    middleware: [
      thunkMiddleware, // Add to middlewares
      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.

// lib/models/api_client.dart
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();

    // The data is JSON encoded only for demonstration
    // Random values are doubles in range [0, 10] with 0.1 step
    var json = jsonEncode({
      'red': (rng.nextDouble() * 101).floorToDouble() / 10,
      'green': (rng.nextDouble() * 101).floorToDouble() / 10,
      'blue': (rng.nextDouble() * 101).floorToDouble() / 10,
    });

    // Simulating async call
    await Future.delayed(Duration(milliseconds: 500));

    // Creating an instance from decoded JSON
    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:

// lib/actions/actions.dart
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

// lib/randomizer.dart
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 {
  @override
  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({@required this.randomize});

  static _ViewModel fromStore(Store<AppState> store) {
    return _ViewModel(
      randomize: () {
        store.dispatch(getBoxColor());
      },
    );
  }
}

And add the widget to the widget tree:

// lib/main.dart
class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
  ...
                    Row(
                      mainAxisAlignment: MainAxisAlignment.spaceBetween,
                      children: <Widget>[
                        // Wrapped the text in a row with the randomizer 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:

// lib/models/api_client.dart
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';

// This is our special exception
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);

    // Throw a ColorException on not desired colors
    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.

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

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:

// lib/actions/actions.dart
import 'dart:async'; // Add the package
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) { // Define the parameter
  return (Store<AppState> store) async {
    try {
      MyBoxColor boxColor = await ApiClient.getBoxColor();
      store.dispatch(SetColor(boxColor));
      completer.complete();   // No exception, complete without error
    } on ColorException catch (e) {
      completer.completeError(e);   // Exception thrown, complete with error
    }
  };
}

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

// lib/randomizer.dart
class _ViewModel {
  final Function(Function(ColorException)) randomize;

  _ViewModel({@required 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.

// lib/randomizer.dart
...
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

27.11.2015

Einführung Cross-Platform Entwicklung

Vor- und Nachteile von Cordova, NativeScript, React Native, Xamarin, etc
lesen
16.11.2015

Die besten deutschsprachigen Hybrid Apps

12 Best Practice Beispiele aus dem deutschen Markt
lesen
10.11.2015

Acht Gründe, sich auf Ionic 2 zu freuen

Ein erster Blick auf die Ionic 2 Developer Preview
lesen
27.11.2015

Einführung Cross-Platform Entwicklung

Vor- und Nachteile von Cordova, NativeScript, React Native, Xamarin, etc
lesen
16.11.2015

Die besten deutschsprachigen Hybrid Apps

12 Best Practice Beispiele aus dem deutschen Markt
lesen

Newsletter abonnieren

Erhalten Sie eine monatliche Zusammenfassung der neuesten Trends in Sachen Cross-platform App Entwicklung.

Jetzt anmelden!