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!
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:
Sounds cool, but what are the advantages of that solution?
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?
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:
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
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:
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! 👇
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.
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.
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);}
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);}
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.
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);
@overrideWidget build(BuildContext context) {return new StoreProvider<List<CartItem>>(store: store,child: new ShoppingCartApp(),);}}
In the above example ShoppingCartApp()
is main application widget.
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]));},);}}
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);
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);})],);}}
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);
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.
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! 🙌