About Redux

Redux is an architecture with a unidirectional data flow that makes it easy to develop applications that are easy to test and maintain.

In Redux there's a Store which holds a State object that represents the state of the whole application. Every application event (either from the user or external) is represented as an Action that gets dispatched to a Reducer function. This Reducer updates the Store with a new State depending on what Action it receives.

And whenever a new State is pushed through the Store the View is recreated to reflect the changes.

With Redux most components are decoupled, making UI changes very easy to make. In addition, the only business logic sits in the Reducer functions. A Reducer is a function that takes an Action and the current application State and it returns a new State object.

About Redux Middleware

The above at first glance appears to be straightforward, but what happens when the application has to perform some asynchronous operation, such as loading data from an external API? This is why people came up with a new component known as the Middleware.

Middleware is a component that may process an Action before it reaches the Reducer. It receives the current application State and the Action that got dispatched, and it can run some code (usually a side effect), such as communicating with a 3rd-party API or data source. Finally, the Middleware may decide to dispatch the original Action, to dispatch a different one, or to do nothing more.

With the Middleware, the above diagram would look like this:

Packages for Redux in Flutter

Taking all this to Flutter, there's two very useful packages we can use, making it really easy and convenient to implement Redux in a Flutter app:

Installing Packages for Redux

First add two dependencies and install the packages:

redux: ^4.0.0
flutter_redux: ^0.6.0

Importing Redux Packages

After that pull in these two plugin in your project:

import 'package:redux/redux.dart';
import 'package:flutter_redux/flutter_redux.dart';

Directory Structure

Now We need to structure our directory like the following:

lib
	actions
	models
	reducers
	pages
	main.dart	

actions

In actions folder we will store all actions. Actions specifies a name about a specific task that will take place. Redux will use this actions to dispatch it. This action will go to reducer. The action has two part, one is the name of the action and another is payload or data. The general structure of a action with some payload looks like the following:

// action/actions.dart

class ActionName{
	int payload1;
	String payload2;
	bool payload3;

	ActionName(this.payload1, this.payload2, this.payload3)
}

If you don't want payload, just the action name, the structure would look like following:

class ActionName{
	ActionName();
}

The class name is used as action name and instance members are used as payloads. That means an action is an instance of a particular class with some data it contains.

The Redux library uses this action instance to send it to the reducer. As your application grows, your app will contain several actions with different name. The reducer will take this action instance and check what type of action it is, and according to the action the reducer will update the state and return it.

To dispatch an action using redux use the following code:

StoreProvider.of<AppState>(context).dispatch(ActionName(payload1, payload2, payload3));

Look at the dispatch method, we are passing an action instance as the argument. That instance becomes the signature of the action that the reducer will get.

models

The models directory is used to store application data model. For example, if your application stores a notebook, you might defines a model that describes about a notebook. A model is nothing but a class with some instance members and optional instance methods.

Here is an example of a model:

// models/NoteBookModel.dart

class NoteBookModel{
	int id;
	String created_at;
	String updated_at;
	String name;
	List<Map<String, dynamic>> notes;

	NoteBookModel({this.id, this.created_at, this.updated_at, this.name, this.notes});

	NoteBookModel.fromJSON(Map<String, dynamic> json){
		id = json["id"];
		created_at = json["created_at"];
		updated_at = json["updated_at"];
		name = json["name"];
		if(json["notes"] == null){
			notes = [];
		}else{
			notes = json['notes'];
		}
	}
}

Your application might contain several models to describes different kind of data. If a single file requires multiple model, you need to manually import the required models. If your application is big and contains a lots of model it is very tedius work. You can create a file called models.dart, that imports all the models and export all of them. So that in your dart file, you will only need to import the models.dart file to access any model you want, instead of seperately importing them. Here is how you can do it:

// models/models.dart

export 'app_state.dart';
export 'NoteBookModel.dart';

Now, if you import models.dart, the model NoteBookModel will be available automatically.

In Redux, the application state is represented as a Model. So one of the file in models directory must be dedicated for Redux state. Create a model that describes Redux state. Here is an example:

// models/app_state.dart

import "NoteBookModel.dart";

@immutable
class AppState {	
  	// User Data --
  	int userid;
  	String email;
  	NoteBookModel selectedNoteBook;
  	int selectedNoteBookId;
	List<NoteBookModel> notebooks;


	// Constructor --
	AppState();

	AppState.initial(){
		userid = 0;
		email = "";
		selectedNoteBook = null;
		selectedNoteBookId = null;
		notebooks = [];
	}

	AppState.fromAppState(AppState state){
		userid = state.userid;
		email = state.email;
		selectedNoteBook = state.selectedNoteBook;
		selectedNoteBookId = state.selectedNoteBookId;
		notebooks = state.notebooks;
	}
}

Redux library will use this AppState model as single source of data which represents the state of the application. In the above model we have two named constructor, leter we will know their use cases.

reducers

The directory reducers will contain reducers. A reducer is nothing but a pure function. When you dispatch an action, the Redux library will call the reducer function and pass the action as an argument. Here is an example:

// reducers/reducer.dart

import '../models/models.dart';
import "../actions/actions.dart";

AppState appReducer(AppState state, action){

	AppState newState = AppState.fromAppState(state);

	if(action is InitUser){
		newState.userid = action.userid;
		newState.email = action.email;
	}else if(action is LoginUser){
		// Modify the state 
	}else if(action is LogoutUser){
		// Modify the state 
	}else if(action is ActionName){
		// Modify the state 
	}

	return newState;
}

In the above example we have created a file reducer.dart which will contain the reducer function. The reducer function appReducer has two parameters, one is state and another is action. This function is called automatically when you dispatch an action. The return type of the reducer function must be same as the type of state. The type of the state is AppState, thats why the return type is AppState. The reducer function will return the modified version of the state.

The first statement is:

AppState newState = AppState.fromAppState(state);

This statement creates a new state newState from the old state state. We have used fromAppState named constructor to make a new instance of the state. This is necessary because the state is immutable and you should not modify it directly. Instead create a new modified version of the state and return it from the reducer function.

After that we are checking what kind of action is dispatched, and according to the action the modification will take place. In the above example we have updated the userid and email field of the state from the action payloads.

newState.userid = action.userid;
newState.email = action.email;

We are taking userid and email from the payload and assigning it to the new state newState. Finally we are returning the newState from the reducer function.

return newState;

Initialization of Store

Before using Redux you must create store and initialize it with reducer and state. You should do it in main.dart file and before runApp call. Here is an example:

// main.dart

// Import Redux packages --
import 'package:redux/redux.dart';
import 'package:flutter_redux/flutter_redux.dart';

// Import models --
import 'models/models.dart';

// Import reducer --
import 'reducers/app_reducer.dart';

// Creating Redux Store --
final store = Store<AppState>(
	appReducer,
	initialState: AppState.initial(),
);

The Redux package provides Store() method to create a Redux store. The first argument is the reducer function. Second argument is the initial state. Here are are using initial() named constructor to create a AppState object with initial values.

StoreProvider()

The Redux library comes with StoreProvider widget, that makes it easy to inject the store in all the child in the widget tree.

runApp(StoreProvider(store: store, child: MyApp()));

StoreConnector()

Now you have done setting up Redux in your project with creating models, reducers, actions and creating store and then using StoreProvider widget to inject the store in child widget tree. Now let's see how we can listen to the changes and dispatch an action.

StoreConnector Widget lets you build widget based on the Redux state. This widget has two property, converter and builder. The general structure of the widget looks like following:

// pages/page.dart

import 'package:flutter_redux/flutter_redux.dart';
import '../models/models.dart';

StoreConnector<AppState, String>(
	converter: (store) (
		return store.state.email;
	),
	builder: (context, viewModel){
		return Text(viewModel);
	}
),

Note that the type <AppState, String> after the widget name. The first type represents the type of the state that the converter will receive as the parameter. In this case it is always AppState as we have defined our state in AppState class. The second type specifies the type of the data that the converter will return. As the converter returns a string (store.state.email), we have specified the second parameter as String.

The converter function runs first and it returns a value. That returned value goes to the builder function as the parameter viewModel.

So the idea is the converter function convert the state into a view model and returns it. And that view model as passed to builder function as the second argument so that the builder function uses that value and decides what UI should be drawn. Depending on the view model passed to the builder function, you can return different widget.

There are another two parameter you can use which is distinct and ignoreChange. These two parameter is used together to decide whether it should call builder methods. When you update the state the StoreConnector will call converter and builder function to rebuilt the UI to update the view with the new state. The problem is when you have many StoreConnector in your page, all of them will get rebuilt whenever you update the state regardless whether a StoreConnector depends on a particular part of your state. For example, if you change the userId in your state and return it, the StoreConnector that only uses userName will also get rebuilt. You certainly don't want to rebuilt those StoreConnector that doesn't need to rebuilt. To avoid this, you can add a filter to your StoreConnector that will check the new state before it call converter and builder methods to determine if your StoreConnector should be rebuilt.

To add a filter add distinct property and set it to true. Then you need to use ignoreChange property and pass a callback. This callback will receive the new state as an argument. If you return true from that callback, it will ignore rebuilding the UI. If you return false, the StoreConnector will rebuilt its child.

Here is an example:

int userid;

StoreConnector<AppState, Map<String, dynamic>>(
	distinct: true,
	converter: (store){
		Map<String, dynamic> user = Map();
		user['userid'] = store.state.userid;
		user['email'] = store.state.email;

		return user;
	},
	builder: (context, user){
		if(user['userid'] == 0){
			userid = 0;
			return new SignIn();
		}else{
			userid = user["userid"];
			return new Notes();
		}
	},
	ignoreChange: (state){
		if(userid == state.userid){
			// userid is same, ignore unncessary build --
			return true;
		}else{
			// userid has changed, the StoreConnector must build --
			return false;
		}
	},
)

In the above example, the ignoreChange callback has only one argument which is the new state. So there should be another variable to check with the previous value. Hence we have manually defined a variable userid at the top and within the builder callback, we are assigning the latest value to the variable userid.

Dispatching Action

The StoreConnector widget rebuilds itself whenever the state changes. And to change the state, you need to dispatch an action. To dispatch an action use the following code:

import 'package:flutter_redux/flutter_redux.dart';
import '../actions/actions.dart';

StoreProvider.of<AppState>(context).dispatch(ActionName(payloads));

When the above statement runs, the reducer function gets executed. The reducer function now creates a new state which is the modified version of the old state and returns the new modified state. Whenever reducer function returns a new state, it will cause StoreConnector to receives the new state and rebuild the widget, the converter and builder altogether.

You can dispatch action either from builder function of the StoreConnector widget or from anywhere of the code.