By Carlos Lira

Most of people only meet Redux when they bump into React, and internalize the architecture as a React thing… This is very sad because it’s f****** awesome! It’s a unidirectional data flow architecture made for any User Interface a.k.a. a UI layer, it allows you to attach functions that will do things such as send an HTTP request to an API and dispatch the response to your state changer – we’ll discuss this in a while. But it’s more useful when combined with a declarative view implementation that can infer the UI updates from state changes, such as Flutter!

A brief explanation before we go on. Redux let you include custom functions that processes the action before it’s dispatched to the next dispatcher and eventually to the reducer to form the new state. These functions are called middleware.

They really come in handy in many scenarios, for example: You make calls to an API and the response of those calls affect your app state. What should you do? First of all, use Redux, it’ll make your life a lot easier, all the data that matters to the UI will be centralized in a state singleton and be available whenever you want. As you know, flutter uses Dart, there are two middleware that will help us out: redux_api_middleware and redux_thunk.

The original thunk middleware is a javascript implementation that you can find here. The thunk middleware that we’ll use was ported to Dart by Brian Egan. The original API middleware is also a javascript implementation that you can find here The API middleware was ported by me to Dart, check it out on my GitHub 😀

The thunk middleware lets you dispatch async calls, which is perfect for the API middleware, because HTTP requests are asynchronous. The API middleware does exactly what you’re thinking, it calls the API and dispatches the response to your reducer so you can figure out the next app state.

We’ll also add a logging middleware so we can easily see the traffic going through our middleware.

  1. Create a new Flutter project.
  2. Add the dependencies.
  3. Create the Redux related files, such as actions and reducers.
  4. Edit the app entry point.
  5. Add a API request logger.
  6. Create the app routes.
  7. Create the UI components.

In this tutorial I’ll be using the VSCode flutter extension. That being said, let’s code!

  1. Invoke View > Command Palette.
  2. Type “flutter, and select the Flutter: New Project.
  3. Enter a project name, such as myapp , and press Enter.
  4. Create or select the parent directory for the new project folder.
  5. Wait for project creation to complete and the main.dart file to appear.

Your flutter app searches for its dependencies in pubspec.yaml dependencies section. Let’s change this file to add our dependencies. We’ll need redux, flutter_redux, redux_thunk and redux_api_middleware.

As you already know, the Flutter community is growing every day and so are the number of libraries and components out there for you to use in your app. These community libraries and components potentially have vulnerabilities that you might end up adding to your app if you’re not careful.

To avoid this always do some research before using them, check how many people are using them, always use the updated version because previous versions could have already fixed vulnerabilities and, if you feel comfortable, audit the code searching for possible vulnerabilities.

That being said, at the time of writing my pubspec.yaml file looked like this:

name: flutter_with_redux
description: A new Flutter project.
version: 1.0.0+1

environment:
  sdk: ">=2.1.0 <3.0.0"

dependencies:
  flutter:
    sdk: flutter
  redux: ^3.0.0
  flutter_redux: ^0.5.3
  redux_thunk: ^0.2.1
  redux_api_middleware: ^0.1.8

dev_dependencies:
  flutter_test:
    sdk: flutter

flutter:
  uses-material-design: true

First of all let’s create the base folder structure for our app. Create these folders in the lib folder: models, actions, reducers and components. After that, your lib folder structure should look like this:

lib
├── actions
├── components
├── models
├── reducers
└── main.dart

The names are kind of suggestive, but it doesn’t hurt to talk a little bit about them.

The models folder is where our data mapping will be, I like to separate them by modules, for example: We have a user module, so all models that concern that module should be created inside the user folder in the models folder.

The actions folder is where our actions will be, I like to split them by modules, for example: We have a user module, so all actions that concern that module should be in a user_actions.dart file in the actions folder.

The reducers folder is where our reducers will be, I like to split them by modules, for example: We have a user module, so all reducers that concern that module should be in a user_reducer.dart file in the reducers folder.

The components folder is where our components will be, but the way I like to organize it is certainly controversial, just like all the other folders I also separate the components as modules, instead of the typical separation of presentation and containers. I’m not against it, I just prefer the modules organization. If you prefer, you can use the presentation and containers approach, but in this article I’ll be using the modules separation.

I’ll use the approach described above, but feel free to modify it as you like. Create a user folder in the models folder, then create a user.dart and a user_state.dart in the user folder.

class User {
  final int id;
  final String name;

  User({
    this.id,
    this.name,
  });

  factory User.fromJSON(Map<String, dynamic> json) => User(
    id: json['id'] as int,
    name: json['name'] as String,
  );
}

class UserDetails {
  final String name;
  final String email;
  final String website;

  UserDetails({
    this.name,
    this.email,
    this.website,
  });

  factory UserDetails.fromJSON(Map<String, dynamic> json) => UserDetails(
    name: json['name'] as String,
    email: json['email'] as String,
    website: json['website'] as String,
  );
}

Now create a user_state.dart in the user folder.

import 'package:flutter_with_redux/models/user/user.dart';

class UserState {
  ListUsersState list;
  UserDetailsState details;

  UserState({
    this.list,
    this.details,
  });

  factory UserState.initial() => UserState(
    list: ListUsersState.initial(),
    details: UserDetailsState.initial(),
  );
}

class ListUsersState {
  dynamic error;
  bool loading;
  List<User> data;

  ListUsersState({
    this.error,
    this.loading,
    this.data,
  });

  factory ListUsersState.initial() => ListUsersState(
    error: null,
    loading: false,
    data: [],
  );
}

class UserDetailsState {
  dynamic error;
  bool loading;
  UserDetails data;

  UserDetailsState({
    this.error,
    this.loading,
    this.data,
  });

  factory UserDetailsState.initial() => UserDetailsState(
    error: null,
    loading: false,
    data: null,
  );
}

The app state model centralizes the entire application state in a singleton, including the user state described above. Create an app_state.dart in the models folder.

import 'package:meta/meta.dart';

import 'package:flutter_with_redux/models/user/user_state.dart';

@immutable
class AppState {
  final UserState user;

  AppState({
    this.user,
  });

  factory AppState.initial() => AppState(
    user: UserState.initial(),
  );

  AppState copyWith({
    UserState user,
  }) {
    return AppState(
      user: user ?? this.user,
    );
  }
}

Redux Standard API-calling Actions a.k.a. RSAAs are the type of actions that the redux_api_middleware intercepts and contain the request definition. We’ll be using a rest API sample provided by JSONPlaceholder for this tutorial. Create a user_actions.dart in the actions folder.

import 'package:redux/redux.dart';
import 'package:redux_thunk/redux_thunk.dart';
import 'package:redux_api_middleware/redux_api_middleware.dart';

import 'package:flutter_with_redux/models/app_state.dart';


const LIST_USERS_REQUEST = 'LIST_USERS_REQUEST';
const LIST_USERS_SUCCESS = 'LIST_USERS_SUCCESS';
const LIST_USERS_FAILURE = 'LIST_USERS_FAILURE';

RSAA getUsersRequest() {
  return
    RSAA(
      method: 'GET',
      endpoint: 'http://jsonplaceholder.typicode.com/users',
      types: [
        LIST_USERS_REQUEST,
        LIST_USERS_SUCCESS,
        LIST_USERS_FAILURE,
      ],
      headers: {
        'Content-Type': 'application/json',
      },
    );
}

ThunkAction<AppState> getUsers() => (Store<AppState> store) => store.dispatch(getUsersRequest());


const GET_USER_DETAILS_REQUEST = 'GET_USER_DETAILS_REQUEST';
const GET_USER_DETAILS_SUCCESS = 'GET_USER_DETAILS_SUCCESS';
const GET_USER_DETAILS_FAILURE = 'GET_USER_DETAILS_FAILURE';

RSAA getUserDetailsRequest(int id) {
  return
    RSAA(
      method: 'GET',
      endpoint: 'http://jsonplaceholder.typicode.com/users/$id',
      types: [
        GET_USER_DETAILS_REQUEST,
        GET_USER_DETAILS_SUCCESS,
        GET_USER_DETAILS_FAILURE,
      ],
      headers: {
        'Content-Type': 'application/json',
      },
    );
}

ThunkAction<AppState> getUserDetails(int id) => (Store<AppState> store) => store.dispatch(getUserDetailsRequest(id));

The RSAAs dispatch Flux Standard Actions a.k.a. FSAs that contain the dispatched type, the response payload and an error, should one occur. This FSA will be dispatched to the user reducer and the reducer will return a new state based on the FSA. Create a user_reducer.dart in the reducers folder.

import 'dart:convert';

import 'package:redux_api_middleware/redux_api_middleware.dart';

import 'package:flutter_with_redux/actions/user_actions.dart';

import 'package:flutter_with_redux/models/user/user.dart';
import 'package:flutter_with_redux/models/user/user_state.dart';

UserState userReducer(UserState state, FSA action) {
  UserState newState = state;

  switch (action.type) {
    case LIST_USERS_REQUEST:
      newState.list.error = null;
      newState.list.loading = true;
      newState.list.data = null;
      return newState;

    case LIST_USERS_SUCCESS:
      newState.list.error = null;
      newState.list.loading = false;
      newState.list.data = usersFromJSONStr(action.payload);
      return newState;

    case LIST_USERS_FAILURE:
      newState.list.error = action.payload;
      newState.list.loading = false;
      newState.list.data = null;
      return newState;


    case GET_USER_DETAILS_REQUEST:
      newState.details.error = null;
      newState.details.loading = true;
      newState.details.data = null;
      return newState;

    case GET_USER_DETAILS_SUCCESS:
      newState.details.error = null;
      newState.details.loading = false;
      newState.details.data = userFromJSONStr(action.payload);
      return newState;

    case GET_USER_DETAILS_FAILURE:
      newState.details.error = action.payload;
      newState.details.loading = false;
      newState.details.data = null;
      return newState;

    default:
      return newState;
  }
}

List<User> usersFromJSONStr(dynamic payload) {
  Iterable jsonArray = json.decode(payload);
  return jsonArray.map((j) => User.fromJSON(j)).toList();
}

UserDetails userFromJSONStr(dynamic payload) {
  return UserDetails.fromJSON(json.decode(payload));
}

The app reducer combines all the reducers so they’re accessible from the AppState singleton. Create an app_reducer.dart in the reducers folder.

import 'package:flutter_with_redux/models/app_state.dart';
import 'package:flutter_with_redux/reducers/user_reducer.dart';

AppState appReducer(AppState state, action) {
  return AppState(
    user: userReducer(state.user, action),
  );
}

This section is pretty straight forward, our build method will return a StoreProvider from the Redux package that will manage our store and let it be accessible through StoreConnectors that we’ll talk about in a minute. Edit the main.dart file.

import 'package:flutter/material.dart';

import 'package:redux/redux.dart';
import 'package:redux_thunk/redux_thunk.dart';
import 'package:redux_api_middleware/redux_api_middleware.dart';

import 'package:flutter_redux/flutter_redux.dart';

import 'package:flutter_with_redux/logger.dart';
import 'package:flutter_with_redux/routes.dart';

import 'package:flutter_with_redux/models/app_state.dart';
import 'package:flutter_with_redux/reducers/app_reducer.dart';

import 'package:flutter_with_redux/components/user/users_screen.dart';
import 'package:flutter_with_redux/components/user/user_details_screen.dart';

void main() => runApp(App());

class App extends StatelessWidget {
  final store = Store<AppState>(
    appReducer,
    initialState: AppState.initial(),
    middleware: [thunkMiddleware, apiMiddleware, loggingMiddleware],
  );

  @override
  Widget build(BuildContext context) {
    return StoreProvider(
      store: this.store,
      child: MaterialApp(
        title: "Flutter with redux",
        theme: ThemeData(
          primarySwatch: Colors.blue,
        ),
        routes: {
          AppRoutes.users: (context) => UsersScreen(),
          AppRoutes.userDetails: (context) => UserDetailsScreen(),
        },
      ),
    );
  }
}

You’ll notice that the logging middleware, the app routes and the screens are missing. Let’s fix that.

This will be very simple, it’s a middleware that intercepts FSAs and prints their type, payload and error. Create a logger.dart file in the lib folder

import 'package:redux/redux.dart';

import 'package:redux_api_middleware/redux_api_middleware.dart';

void loggingMiddleware<State>(
  Store<State> store,
  dynamic action,
  NextDispatcher next,
) {
  if (action is FSA) {
    print('{');
    print('  Action: ${action.type}');

    if (action.payload != null) {
      print('  Payload: ${action.payload}');
    }

    print('}');
  }

  next(action);
}

This is simply a way to organize our named routes. Create a routes.dart in the lib folder.

class AppRoutes {
  static final users = '/';
  static final userDetails = '/details';
}

The users screen is a StatelessWidget because it doesn’t need a state of it’s own. It connects to the store and has access to all the app data, we only need to map the state into “props”, I’m not sure about that term yet, but It’s what I’m using, once we do that the builder from the StoreConnector should have a props parameter that can be used through our component, and as you can tell we’ll also map the actions so we can easily use them. Create a users_screen.dart in the components/users folder.

import 'package:flutter/material.dart';
import 'package:flutter_redux/flutter_redux.dart';

import 'package:redux/redux.dart';

import 'package:flutter_with_redux/routes.dart';

import 'package:flutter_with_redux/models/app_state.dart';

import 'package:flutter_with_redux/models/user/user.dart';
import 'package:flutter_with_redux/models/user/user_state.dart';

import 'package:flutter_with_redux/actions/user_actions.dart';

class UsersScreen extends StatelessWidget {
  void handleInitialBuild(UsersScreenProps props) {
    props.getUsers();
  }

  @override
  Widget build(BuildContext context) {
    return StoreConnector<AppState, UsersScreenProps>(
      converter: (store) => mapStateToProps(store),
      onInitialBuild: (props) => this.handleInitialBuild(props),
      builder: (context, props) {
        List<User> data = props.listResponse.data;
        bool loading = props.listResponse.loading;

        Widget body;
        if (loading) {
          body = Center(
            child: CircularProgressIndicator(),
          );
        } else {
          body = ListView.separated(
            padding: const EdgeInsets.all(16.0),
            itemCount: data.length,
            separatorBuilder: (context, index) => Divider(),
            itemBuilder: (context, i) {
              User user = data[i];

              return ListTile(
                title: Text(
                  user.name,
                ),
                onTap: () {
                  props.getUserDetails(user.id);
                  Navigator.pushNamed(context, AppRoutes.userDetails);
                },
              );
            },
          );
        }

        return Scaffold(
          appBar: AppBar(
            title: Text('Users list'),
          ),
          body: body,
        );
      },
    );
  }
}

class UsersScreenProps {
  final Function getUsers;
  final Function getUserDetails;
  final ListUsersState listResponse;

  UsersScreenProps({
    this.getUsers,
    this.listResponse,
    this.getUserDetails,
  });
}

UsersScreenProps mapStateToProps(Store<AppState> store) {
  return UsersScreenProps(
    listResponse: store.state.user.list,
    getUsers: () => store.dispatch(getUsers()),
    getUserDetails: (int id) => store.dispatch(getUserDetails(id)),
  );
}

And finally…

The user details screen is a StatelessWidget because it also doesn’t need a state of it’s own. It dynamically calls the API middleware and dispatches states and the UI reflects it beautifully. Create a user_details_screen.dart in the components/users folder.

import 'package:flutter/material.dart';
import 'package:flutter_with_redux/models/user/user.dart';

import 'package:redux/redux.dart';

import 'package:flutter_redux/flutter_redux.dart';

import 'package:flutter_with_redux/models/app_state.dart';
import 'package:flutter_with_redux/models/user/user_state.dart';

class UserDetailsScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return StoreConnector<AppState, UserDetailsScreenProps>(
      converter: (store) => mapStateToProps(store),
      builder: (context, props) {
        UserDetails data = props.detailsResponse.data;
        bool loading = props.detailsResponse.loading;

        TextStyle textStyle = TextStyle(
          height: 2,
          fontSize: 20,
        );

        Widget body;
        if (loading) {
          body = Center(
            child: CircularProgressIndicator(),
          );
        } else {
          body = Center(
            child: IntrinsicWidth(
              child: Column(
                mainAxisAlignment: MainAxisAlignment.center,
                crossAxisAlignment: CrossAxisAlignment.center,
                children: [
                  Text(data.name, style: textStyle),
                  Text(data.email, style: textStyle),
                  Text(data.website, style: textStyle),
                ],
              ),
            ),
          );
        }

        return Scaffold(
          appBar: AppBar(
            title: Text('User details'),
          ),
          body: body,
        );
      },
    );
  }
}

class UserDetailsScreenProps {
  final UserDetailsState detailsResponse;

  UserDetailsScreenProps({
    this.detailsResponse,
  });
}

UserDetailsScreenProps mapStateToProps(Store<AppState> store) {
  return UserDetailsScreenProps(
    detailsResponse: store.state.user.details,
  );
}

Time to run your app and enjoy it…

Most of the APIs that we consume on apps are authenticated somehow. And we don’t want to ask the user’s credentials every time. So we want to store these credentials to make our lives and the user’s life a breeze, but that’s dangerous as other apps could have access to that specific file and it become a possible attack vector. There’s a library called flutter_secure_storage that solves this problem. It uses native Keychain on IOS and KeyStore on Android. So yeah… Use it if you’re storing user’s credentials.

Forget the idea that Redux is a react package! Use it wherever you can, it made my life easier and I hope I did yours a little bit too 😀

I know flutter is a new and fresh piece of technology, but it’s extremely productive, easy to work with and I believe it has a bright future. I was surprised by Dart, it’s very easy to learn and fun to code, reminds me a bit of javascript.

The full project will be in my GitHub if you need it. You can get in touch with me at my LinkedIn if you need any help, criticize or just want to talk about your problems.

If you enjoy this reading, please tell me about it at the comments, and remember the most important thing: Cat gifs get claps…