Flutter State management with Redux

Flutter State management with Redux

What is state management in Flutter?

State management is essentially a way to facilitate communication and sharing of data across widgets. It creates a tangible data structure to represent the state of your app that you can read from and write to.

When it comes to managing state in a flutter application, there are many options, but I find myself relying on stateful widgets and these have a downfall as I have to observe the change of states and rebuild widgets. Flutter is fast, but we should be smart about what we ask Flutter to rebuild! The smartest way to solve this problem is by using state management tools like Bloc, Provider, and Momentum but today I would like to write how we can use Redux to manage states in our flutter application.

While Redux is mostly used as a state management tool with React I found it quite easy to apply it in flutter projects I have contributed to so far. With redux &flutter_redux we can use Redux in our flutter applications, so the state of our application is kept in a store, and each widget can access any state that it needs from this store.

Redux utilities to easily consume a Redux Store to build Flutter Widgets:

  1. StoreProvider - The base Widget. It will pass the given Redux Store to all descendants that request it.
  2. StoreBuilder - A descendant Widget that gets the Store from a StoreProvider and passes it to a Widget builder function.
  3. StoreConnector - A descendant Widget that gets the Store from the nearest StoreProvider ancestor, converts the Store into a ViewModel with the given converter function, and passes the ViewModel to a builder function.

Any time the Store emits a change event, the Widget will automatically be rebuilt. No need to manage subscriptions!

Below is an example of how we can use redux in a simple counter app:
import 'package:flutter/material.dart';
import 'package:flutter_redux/flutter_redux.dart';
import 'package:redux/redux.dart';

// One simple action: Increment
enum Actions { Increment }

// The reducer, which takes the previous count and increments it in response
// to an Increment action.
int counterReducer(int state, dynamic action) {
  if (action == Actions.Increment) {
    return state + 1;
  }

  return state;
}

void main() {
  // Create your store as a final variable in the main function or inside a
  // State object. This works better with Hot Reload than creating it directly
  // in the `build` function.
  final store = Store<int>(counterReducer, initialState: 0);

  runApp(FlutterReduxApp(
    title: 'Flutter Redux Demo',
    store: store,
  ));
}

class FlutterReduxApp extends StatelessWidget {
  final Store<int> store;
  final String title;

  FlutterReduxApp({Key key, this.store, this.title}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    // The StoreProvider should wrap your MaterialApp or WidgetsApp. This will
    // ensure all routes have access to the store.
    return StoreProvider<int>(
      // Pass the store to the StoreProvider. Any ancestor `StoreConnector`
      // Widgets will find and use this value as the `Store`.
      store: store,
      child: MaterialApp(
        theme: ThemeData.dark(),
        title: title,
        home: Scaffold(
          appBar: AppBar(
            title: Text(title),
          ),
          body: Center(
            child: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                // Connect the Store to a Text Widget that renders the current
                // count.
                //
                // We'll wrap the Text Widget in a `StoreConnector` Widget. The
                // `StoreConnector` will find the `Store` from the nearest
                // `StoreProvider` ancestor, convert it into a String of the
                // latest count, and pass that String  to the `builder` function
                // as the `count`.
                //
                // Every time the button is tapped, an action is dispatched and
                // run through the reducer. After the reducer updates the state,
                // the Widget will be automatically rebuilt with the latest
                // count. No need to manually manage subscriptions or Streams!
                StoreConnector<int, String>(
                  converter: (store) => store.state.toString(),
                  builder: (context, count) {
                    return Text(
                      'The button has been pushed this many times: $count',
                      style: Theme.of(context).textTheme.display1,
                    );
                  },
                )
              ],
            ),
          ),
          // Connect the Store to a FloatingActionButton. In this case, we'll
          // use the Store to build a callback that will dispatch an Increment
          // Action.
          //
          // Then, we'll pass this callback to the button's `onPressed` handler.
          floatingActionButton: StoreConnector<int, VoidCallback>(
            converter: (store) {
              // Return a `VoidCallback`, which is a fancy name for a function
              // with no parameters and no return value. 
              // It only dispatches an Increment action.
              return () => store.dispatch(Actions.Increment);
            },
            builder: (context, callback) {
              return FloatingActionButton(
                // Attach the `callback` to the `onPressed` attribute
                onPressed: callback,
                tooltip: 'Increment',
                child: Icon(Icons.add),
              );
            },
          ),
        ),
      ),
    );
  }
}

NB: I wrote this article assuming that you have some experience with React-redux and you understand how actions, reducers, and dispatchers work in react-redux.

Thank you for reading, and let's connect! Thank you for reading my blog. Feel free to subscribe to connect on Twitter