Flutter + Redux — How to make Shopping List App?

Written by pszklarska | Published 2018/05/01
Tech Story Tags: flutter | redux | programming | ios | flutter-and-redux

TLDRvia the TL;DR App

Hi everyone! In this article I’d like to show you how to create Flutter application using Redux. If you don’t know what Flutter is, I encourage you to read my article Flutter — 5 reasons why you may love it . However, if you know what Flutter is and you’d like to create an application that is well designed, easy to test and has very predictable behaviour — then keep going with reading!

What is Redux?

Firstly, let’s start with explaining what Redux is. Redux is an application architecture, made originally for JavaScript and now used in applications built with reactive frameworks (such as React Native or Flutter). Redux is simplified version of Flux architecture, made by Facebook. But what’s all about with Redux? Basically, you need to know three things:

  1. there’s a single source of truth — your whole application state is kept only in one place (called store)
  2. the state is read-only — to change the application state you need to dispatch an action, and then new state is created
  3. changes are made with pure functions — a pure function (to simplify, it’s a function without side effects) takes the previous state and an action, and returns new state

Sounds cool, but what are the advantages of that solution?

  • we have control over the state — it means that we exactly know what caused state change, we don’t have duplicated state and we can easily follow data flow
  • pure reducer functions are easy to test — we can pass state, action and test if result is correct
  • application is clearly structured — we have different layers for actions, models, business logic, etc. — so you exactly know where to put another new feature
  • it’s great architecture for more complicated apps — you don’t need to pass state down the whole view tree from parent to child
  • and there’ one more…

Redux Time Travel

There’s one cool feature possible in Redux — 🎉 Time Travel! With Redux and proper tools you can track your application state over the time, inspect actual state and recreate it at any time. See this feature in action:

Time Travel in action — how cool is that?

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:

  1. Your application has some state at the beginning (number of clicks, which is 0)
  2. Based on that state view is rendered.
  3. If user taps on button, there’s action send (e.g. IncrementCounter)
  4. Action is received by reducer, which knows previous state (counter 0), receives action (IncrementCounter) and can return new state (counter 1)
  5. Your application has new state (counter 1)
  6. 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:

pszklarska/FlutterShoppingCart_FlutterShoppingCart - Flutter example of shopping app using Redux architecture_github.com

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);}

Note: Here’s the full source code for this file.

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);}

Note: Here’s the full source code for this file

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();}

Note: Here’s the full source code for this file.

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));}

Note: Here’s the full source code for this file.

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);

@overrideWidget build(BuildContext context) {return new StoreProvider<List<CartItem>>(store: store,child: new ShoppingCartApp(),);}}

Note: Here’s the full source code for this file.

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 {@overrideWidget 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]));},);}}

Note: Here’s the full source code for this file.

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.

Next, in builder function we get list — which is basically list of CartItems from store, which we can use for building ListView.

Ok, cool — we have reading data here. Now how to set some data?

To do it, we’ll use also StoreConnector, but in a slightly different way.

class AddItemDialog extends StatelessWidget {@overrideWidget build(BuildContext context) {return new StoreConnector<List<CartItem>, OnItemAddedCallback>(converter: (store) { return (itemName) =>store.dispatch(AddItemAction(CartItem(itemName, false))); }, builder: (context, callback) {return new AddItemDialogWidget(callback);});}}

typedef OnItemAddedCallback = Function(String itemName);

Note: Here’s the full source code for this file.

Let’s look at the code. We used StoreConnector, as in the previous example, but this time, instead of mapping list of CartItems, into the same list, we’ll map this into OnItemAddedCallback. This way we can pass callback to the AddItemDialogWidget and call it when user adds some new item:

class AddItemDialogWidgetState extends State<AddItemDialogWidget> {String itemName;

final OnItemAddedCallback callback;AddItemDialogWidgetState(this.callback);

@overrideWidget build(BuildContext context) {return new AlertDialog(...actions: <Widget>[...new FlatButton(child: const Text('ADD'),onPressed: () { ... callback(itemName);})],);}}

Note: Here’s the full source code for this file.

Now, every time user press “ADD” button, the callback will dispatch AddItemAction() event.

Now we can do very similar thing for toggling item state:

class ShoppingListItem extends StatelessWidget {final CartItem item;

ShoppingListItem(this.item);

@overrideWidget build(BuildContext context) {return new StoreConnector<List<CartItem>, OnStateChanged>(converter: (store) {return (item) => store.dispatch(ToggleItemStateAction(item));}, builder: (context, callback) {return new ListTile(title: new Text(item.name),leading: new Checkbox(value: item.checked,onChanged: (bool newValue) {callback(CartItem(item.name, newValue));}),);});}}

typedef OnStateChanged = Function(CartItem item);

Note: Here’s the full source code for this file.

As in the previous example, we use StoreConnector for mapping List<CartItem> into OnStateChanged callback. Now every time checkbox is changed (in onChanged method), the callback fires ToggleItemStateAction event.

Summary

That’s all! In this article we created a simple Shopping List application using Redux architecture. In our application we can add some items and change their state. Adding new features to this application is as simple as adding new actions and reducers.

Here you can check the full source code for this application, including Time Travel widget:

pszklarska/FlutterShoppingCart_FlutterShoppingCart - Flutter example of shopping app using Redux architecture_github.com

Hope you liked this post and stay tuned for more! 🙌


Written by pszklarska | Flutter GDE / Flutter Developer / ex-Android Developer / blogger / speaker / cat owner / travel enthusiast
Published by HackerNoon on 2018/05/01