Redux Widgets on a simple example

All of the above rules makes data flow in Redux unidirectional. But what does it mean? In practice it’s all done with actions, reducers, store and states. Let’s imagine application that shows button counter:

Your application has some state at the beginning (number of clicks, which is 0) Based on that state view is rendered. If user taps on button, there’s action send (e.g. IncrementCounter) Action is received by reducer, which knows previous state (counter 0), receives action (IncrementCounter) and can return new state (counter 1) Your application has new state (counter 1) Based on new state, view is rendered again

So as you can see, generally it’s all about the state. You have single app state, the state is read-only for view, and to create new state you need to send action. Sending action fires reducer that creates and emits new application state. And history repeats itself.

Redux Data Flow

Example of Shopping List App with Redux

Let me show how Redux works in practice on more advances example. We’ll create a simple ShoppingCart application. In this application there will be functionalities for:

adding items

marking items as checked

and that’s basically all 😎

The application will look like this:

You can see the whole application code on GitHub:

Let’s start with coding! 👇

Prerequisite

In this article I’ll not show creating UI for this application. You can check the code for this Shopping List application before implementing Redux here. We’ll start with coding from this point and we’ll add Redux to this application.

If you’ve never used Flutter before, I encourage you to try a Flutter Codelabs from Google.

Setup

To run with Redux on Flutter, you need to add dependencies to your pubspec.yaml file:

flutter_redux: ^0.5.2

You can check the newest version on flutter_redux package page.

Model

Our application needs to manage adding and changing items, so we‘ll use simple CartItem model to store single item state. Our whole application state will be just list of CartItems. As you can see, CartItem is just a plain Dart object.

class CartItem {

String name;

bool checked;



CartItem(this.name, this.checked);

}

Actions

Firstly, we need to declare actions. Action is basically any intent that can be invoked to change application state. In our application we’ll have two actions, for adding and changing item:

class AddItemAction {

final CartItem item;



AddItemAction(this.item);

}



class ToggleItemStateAction {

final CartItem item;



ToggleItemStateAction(this.item);

}

Reducers

Then, we need to tell our application what should be done with those actions. This is why reducers are for — they simply take current application state and the action, then they create and return new application state. We’ll have two reducers methods:

List<CartItem> appReducers(List<CartItem> items, dynamic action) {

if (action is AddItemAction) {

return addItem(items, action);

} else if (action is ToggleItemStateAction) {

return toggleItemState(items, action);

}

return items;

}



List<CartItem> addItem(List<CartItem> items, AddItemAction action) {

return List.from(items)..add(action.item);

}



List<CartItem> toggleItemState(List<CartItem> items, ToggleItemStateAction action) {

return items.map((item) => item.name == action.item.name ?

action.item : item).toList();

}

Method appReducers() delegates the action to proper methods. Both methods addItem() and toggleItemState() return new lists — that’s our new application state. As you can see, you shouldn’t modify current list. Instead of it, we create new lists every time.

StoreProvider

Now, when we have actions and reducers, we need to provide place for storing application state. It’s called store in Redux and it’s single source of truth for our application.

void main() {

final store = new Store<List<CartItem>>(

appReducers,

initialState: new List());



runApp(new FlutterReduxApp(store));

}

To create store, we need to pass reducers methods and initial application state. If we created the store, we must pass it to the StoreProvider to tell our application than it can be used by anyone who wants to request app state:

class FlutterReduxApp extends StatelessWidget {

final Store<List<CartItem>> store;



FlutterReduxApp(this.store);



@override

Widget build(BuildContext context) {

return new StoreProvider<List<CartItem>>(

store: store,

child: new ShoppingCartApp(),

);

}

}

In the above example ShoppingCartApp() is main application widget.

StoreConnector

Currently we have everything except… actual adding and changing items. How to do that? To make it possible, we need to use StoreConnector. It’s a way to get the store and make some action with it or read it’s state.

Firstly, we’d like to read current data and show this in a list:

class ShoppingList extends StatelessWidget {

@override

Widget build(BuildContext context) {

return new StoreConnector<List<CartItem>, List<CartItem>>(

converter: (store) => store.state,

builder: (context, list) {

return new ListView.builder(

itemCount: list.length,

itemBuilder: (context, position) =>

new ShoppingListItem(list[position]));

},

);

}

}

Code above wraps default ListView.builder with StoreConnector . StoreConnector can take current app state (which is List<CartItem> ) and map this with converter function to anything. For purposes of this case, it’ll be the same state ( List<CartItem> ), because we need the whole list here.