Counter Redux Cycle

Now, we have all the boiler plate we need to implement an entire Redux cycle. If anything hasn't made any sense yet, this should clear it up.

Take a look at this visual again from Ignacio Chavez

redux flow

I personally like to build my redux cycles starting at the Action, then implementing middleware (if any), the reducer, and finally the views which dispatch the actions.

1. Add the Counter Action

Start by adding the proper files. Create a new actions directory with a counter_actions.dart file:

  1. // lib folder:
  2. - actions
  3. -counter_actions.dart
  4. - models
  5. - app_state.dart
  6. - pages
  7. - home_page.dart
  8. - reducers
  9. - app_reducer.dart
  10. main.dart

Open the counter_actions file:

NB: I'm imagining that our app can also decrease count to make a point:

  1. // actions/counter_actions.dart
  2. class IncrementCountAction {}
  3. class DecrememtCountAction {}

That's literally the entire file. Many actions will be more complicated than this, but many actions are simply declarations that the action is an option.

Because the reducers will only mutate state when given a pre-defined action, you must create a new class for each possible action, even if it does nothing else. This is desirable because it means nothing can mutate your state on accident. You had to have explicitly coded in a possible action before your reducers will acknowledge them.


NB:The way I'm structuring my files is preference. I like to make things as modular as possible. In most examples of Flutter_Redux, you'll see all of the actions in a single file — and that's completely fine.


2. Add a Counter Reducer

Again, you need to add the files:

  1. // lib folder:
  2. - actions
  3. - counter_actions.dart
  4. - models
  5. - app_state.dart
  6. - pages
  7. - home_page.dart
  8. - reducers
  9. - app_reducer.dart
  10. - counter_reducer.dart //new
  11. main.dart

Then in the new counter_reducer file:

NB: Still pretending our app can decrement.

  1. import 'package:me_suite/actions/counter_actions.dart';
  2. int counterReducer(int currentCount, action) {
  3. if (action is IncrementCountAction) {
  4. currentCount++;
  5. return currentCount;
  6. } else if (action is DecrememtCountAction) {
  7. currentCount--;
  8. return currentCount;
  9. } else {
  10. return currentCount;
  11. }
  12. }

All that's going on here is the counterReducer is checking which action has been dispatched by the appReducer, and mutating the slice of state that's concerned with. The counter reducer is passed in state.count from the appReducer, not the entire state. So all it needs to do is change the state and return that slice. Very modular. Very nice.

So far, we've created an action so that the reducer knows it's an allowed mutation, and created a reducer to actually perform that.


NB: We skipped middleware, because this cycle doesn't need any middleware.


3. Add the Views

Finally, we have all this, but there needs to be a button of some sort that will actually make this cycle fire.

First, make a new directory called containers.

Within that directory, make a directory called counter.

Then, create a file called increase_counter.dart.

  1. // lib folder:
  2. - actions
  3. - counter_actions.dart
  4. - containers // new
  5. - counter // new
  6. - increase_counter.dart // new
  7. - counter.dart // new
  8. - models
  9. - app_state.dart
  10. - pages
  11. - home_page.dart
  12. - reducers
  13. - app_reducer.dart

Containers are an idea that I learned when I started with Redux in JS-world that I really like. Each container is like a wrapper widget that's 'smart'. It's aware of the state, and it has the ability to dispatch an action to Redux to start a redux cycle. This wrapper wraps a 'dumb' view widget.

In our case, the views are so simple that we'll combine a container with it's dumb component.

  • The increase_counter widget is a smart widget that displays a button, that on pressed, dispatches an action to the store.
  • The counter widget simply displays the current count in the state. It has no ability to change the state.

There's a lot going on here. The important Redux-y parts are:

  • StoreConnector - This simply provides the widget access to the store
  • StoreConnector.converter - This is the callback that will be called when the user interacts with view. It always takes a 'store'
  • store.dispatch() method - This is a built in method that tells Redux to begin a redux cycle. It always takes a new YourActionHere()
  • StoreConnector.builder - This is a flutter builder method. It always takes BuildContext and a callback. The call back is the one defined in the converter arg. It's automatically wired up by redux. - builder methods simply return widgets.
  1. // containers/increase_counters.dart
  2. import 'package:flutter/material.dart';
  3. import 'package:flutter_redux/flutter_redux.dart';
  4. import 'package:me_suite/actions/counter_actions.dart';
  5. import 'package:me_suite/models/app_state.dart';
  6. import 'package:redux/redux.dart';
  7. // This is just another stateless widget.
  8. class IncreaseCountButton extends StatelessWidget {
  9. IncreaseCountButton({Key key}) : super(key: key);
  10. @override
  11. Widget build(BuildContext context) {
  12. return new StoreConnector<AppState, VoidCallback>(
  13. converter: (Store<AppState> store) {
  14. return () {
  15. store.dispatch(new IncrementCountAction());
  16. };
  17. },
  18. builder: (BuildContext context, VoidCallback increase) {
  19. return new FloatingActionButton(
  20. onPressed: increase,
  21. child: new Icon(Icons.add),
  22. );
  23. },
  24. );
  25. }
  26. }

This is a very basic container, but all containers follow this pattern. The only real difference between this and more complex flutter patterns is that rather than returning a new FloatingActionButton in your builder, you'd most likely return a custom widget that takes in some data from the store. That's a dumb component, but that doesn't apply here.

4. Build the Counter View

The counter view adds yet another redux concept — but this is the last one! (for now)

Most of this file — especially the Counter class, looks the same. But we have added a second class: _viewModel.

These viewModel helper classes are just that — a model for your view's data (from the store).

Whenever you're creating a container widget that displays data from the state that will be updated, you should use a viewModel helper class.

These viewModel classes will use a Redux method called fromStore(), which gets the data from the store, and knows to re-render when the slices of relevant state are updated.

  1. // containers/counter/counter.dart
  2. import 'package:flutter/foundation.dart';
  3. import 'package:flutter/material.dart';
  4. import 'package:flutter_redux/flutter_redux.dart';
  5. import 'package:me_suite/models/app_state.dart';
  6. import 'package:redux/redux.dart';
  7. class Counter extends StatelessWidget {
  8. @override
  9. Widget build(BuildContext context) {
  10. return new StoreConnector<AppState, _ViewModel>(
  11. // Rather than build a method here, we'll defer this
  12. // responsibilty to the _viewModel.
  13. converter: _ViewModel.fromStore,
  14. // Our builder now takes in a _viewModel as a second arg
  15. builder: (BuildContext context, _ViewModel vm) {
  16. return new Text(
  17. // Our _viewModel is 'watching' the count slice of state
  18. // So this will be rerendered when that slice of
  19. // state changes
  20. vm.count.toString(),
  21. style: Theme.of(context).textTheme.display1,
  22. );
  23. }
  24. );
  25. }
  26. }
  27. // This is just a class -- nothing fancy
  28. class _ViewModel {
  29. // It should take in whatever it is you want to 'watch'
  30. final int count;
  31. _ViewModel({
  32. @required this.count,
  33. });
  34. // This is simply a constructor method.
  35. // This creates a new instance of this _viewModel
  36. // with the proper data from the Store.
  37. //
  38. // This is a very simple example, but it lets our
  39. // actual counter widget do things like call 'vm.count'
  40. static _ViewModel fromStore(Store<AppState> store) {
  41. return new _ViewModel(count: store.state.count);
  42. }
  43. }

Add the Counter Widgets into the App

With these two widgets, all we've really done is make a button with fancy capabilities to dispatch actions to Redux, and a text widget with fancy capabilities to get information from our Redux Store. Let's add this button and text to our app so we can see em in action.

  1. // pages/home_page.dart
  2. import 'package:flutter/material.dart';
  3. import 'package:me_suite_code_along/containers/counter/counter.dart';
  4. import 'package:me_suite_code_along/containers/counter/increase_counter.dart';
  5. class HomePage extends StatelessWidget {
  6. final String title;
  7. HomePage(this.title);
  8. @override
  9. Widget build(BuildContext context) {
  10. return new Scaffold(
  11. appBar: new AppBar(
  12. title: new Text(this.title),
  13. ),
  14. body: new Container(
  15. child: new Center(
  16. child: new Column(
  17. mainAxisAlignment: MainAxisAlignment.center,
  18. children: <Widget>[
  19. new Text(
  20. 'You have pushed the button this many times:',
  21. ),
  22. new Counter(), // updated
  23. ],
  24. ),
  25. ),
  26. ),
  27. floatingActionButton: new IncreaseCountButton() // updated
  28. );
  29. }
  30. }

The result:

Reload your app, press that Increment button, and watch that number rise.