Managing state in Flutter using Redux

Flutter allows us to manage the state of the widgets individually. However, as the complexity of an app grows, and the need to allow different widgets to have access to the state of one another arises, there comes a need to store the state of the app in one commonplace. Redux fits this particular need perfectly. This post explains how Redux can be used with Flutter.

The Redux pattern, which is popularly used with React, can be used in such circumstances. This allows us to have a single source of truth that is available throughout an app. But before we learn to use Redux with Flutter, understanding how Redux works helps.

The Redux Architecture

Redux has four major components, namely,

  1. The state
  2. Actions
  3. Dispatcher
  4. Reducer
The redux architecture
The Redux architecture

The state is where the state (all the data) of an app gets stored. Redux libraries usually provide us with what are called providers to help us bind the state to our views. However, modifying the store is not straightforward (and it’s good) and this helps us have a unidirectional flow of data.

To modify the store, the dispatcher (which is provided by Redux) has to fire an action. This action has a type and the data that we want to modify the store with. Once the action is fired, the reducer makes a copy of the store, updates the copy with the new data and returns the new state. Then, the view is updated with the new data.

By incorporating this Redux pattern into a Flutter app, we can ensure that we have a common app state that can be mutated by firing actions. Even though I say the state is mutated, do note that a Redux store is immutable by design and every time an action is fired a new state is returned and the old one remains intact.

Installing Redux

There are two Flutter packages, redux and flutter_redux, that help us use Redux with Flutter, and they should be installed first. So, in the pubspec.yaml file, list these two packages under dependencies.

dependencies:
  flutter:
    sdk: flutter
  flutter_redux: ^0.5.3
  redux: ^3.0.0

Now, you need to run flutter pub get in order to install these packages. Once done, we can start using Redux in our Flutter app. To demonstrate this, I am going to use the default demo app created by the flutter create command.

The default app has a counter that can be incremented by pressing on the floating action button. The counter is contained within the state of the MyHomePage widget. Let’s try to move this into a Redux store and increment it by dispatching actions.

Creating a Model

Before creating the store, we need to create a model of the counter. To that end, create a dart file called model and create a class called Counter. This class will have a property called counter which will be storing, as the name implies, the counter. Create a constructor that would accept an integer as an argument and assign it to the counter property. This can be easily accomplished in Dart by passing the name of the property—to which we want to assign the passed argument—preceded by this. as a parameter. Now, we have a model for our counter.

class Counter{
  int counter;
  Counter(this.counter);
}

Creating a State

Next, let’s create the state of our app. Create a Dart file called state and create a class called AppState. An instance of this class will be holding the state of our app. Now, we need to store the counter in the app’s state. So, import the Counter model into the state file and create a property of the type Counter called counter. Then, create a constructor that would accept a Counter object as an argument and assign it to the counter property.

To initialize the state of the app, it is advisable to create a named constructor that would set the value of the counter to zero.

import 'package:sample/model.dart';
 
class AppState {
  final Counter counter;
  AppState(this.counter);
  AppState.initial():counter=new Counter(0);
}

Creating an Action

Now that we have created our AppState class, let’s create an action to modify the state. Create a new file called action.dart and create a new class called IncrementAction. This action will carry the data that would be used by the reducer to modify the state with. Since we want to increment the counter, we need a counter property in this class. So, create an integer property called counter and initialize it using the constructor.

class IncrementAction{
  final int count;
  IncrementAction(this.count);
}

Creating a Reducer

Next, we need to create a reducer that would return the updated state. Therefore, create a new file called reducer.dart and create a function that would return an instance of our AppState class. This function accepts the state object and an object of the fired action as arguments. When an action is fired, Redux calls this reducer function and passes the current state of the app and an instance of the action that was fired as the arguments.

We can modify the passed state object with the data in the action object, create a new instance of the AppState with the modified state object, and return it. But this reducer function will be called every time an action is fired. Different actions carry different data and are supposed to modify the state in different ways. So, how do we perform different functions based on the action fired? Since every action is an instance of an action class, we can check for the data type of the action object and then, decide on the subsequent course of actions.

We can check the type of a variable in Dart by using the is syntax. So, we can write a conditional statement to see if an action is of a certain type and perform the necessary actions.

AppState appStateReducer (AppState state, dynamic action){
    if(action is IncrementAction){
      return new AppState(new Counter(action.count));
    }
    return state;
}

As shown above, we can write a reducer function that checks if the action fired is an instance of our action class IncrementAction, and return a new AppState instance initialized with the updated counter object.

The View Model

Now, we have created an action, an app state, and a reducer. All that is left to do is to create a button click event that would fire this action. But before we do this, we need to create a view model.

The view model is not all that complex. It acts as a presentation layer between the state of our app and the user interface of an app. Now, the way we want to store our data in the state of our app may not necessarily be the way we want to display it. For instance, we may store the first name and the last name of a user in separate variables in the state. But when displaying the name of a user, we might want to display both the first name and the last name together. We can use the view model to do such cosmetic changes to our data. In other words, we use the view model to help both the state and the UI interact with one another. It simply acts as a filter.

The Redux architecture with the view model
How View Model fits into the Redux architecture

Creating a View Model

Let’s go ahead and create a new file called viewModel.dart and create a class called ViewModel. Now, we want our UI to do two things: to display the counter value and fire our IncrementAction action. So, our view model should include a variable to store the counter value and a method to fire the action.

So, let’s create an integer called counter and a method called onIncrement. Let’s also create a constructor that would initialize these two.

Next, we need to create a factory constructor that would return an instance of the ViewModel class. The factory constructor ensures if an instance of the ViewModel class already exists, then that instance is returned instead of creating and returning a new instance of it.

This constructor should accept a store object as an argument. Then, we shall instantiate the ViewModel class and return it. But before we do that, we need to get the value of the counter and implement a function to fire the IncrementAction action.

We can get the counter value from the store object that is passed as an argument. The state of the app is stored in the attribute called state. So, we can access the counter value by using store.state.counter.counter. The state has a counter property of type Counter which has an integer attribute called counter.

Then create a method to dispatch the action. The dispatch method is attached to the store object and can be accessed via store.dispatch(). To dispatch an action, we need to create an object of the action class and pass it as an argument into the dispatch method. As you may remember, this action class also carries the necessary data. In our case, we have a property in our action class called counter which will carry the updated value of the counter.

Incrementing the counter from within our view model

So, we can change the counter value by instantiating our action class with the new counter value. Since we are trying to increment our counter value, we can get the existing value of the counter from the store, increment it by one, and pass it as an argument into our action class constructor. We can pass the returned object into the dispatch method.

You will have to import the redux package, and the actions and the state file.

import 'package:redux/redux.dart';
import 'package:sample/actions.dart';
import 'package:sample/state.dart';
 
class ViewModel{
  int count;
  final Function () onIncrement;
  ViewModel(this.count,this.onIncrement);
 
  factory ViewModel.create(Store<AppState> store){
    _onIncrement(){
      print("Incrementing");
      print(store.state.counter.counter.toString());
      store.dispatch(new IncrementAction(store.state.counter.counter+1));
    }
  
    return ViewModel(store.state.counter.counter,_onIncrement);
  }
}

Creating a Store object and passing it down the Flutter Widget tree

We are almost done. Now, we need to create a store object and pass it down our widget tree. Then we can access our store in our widgets using a store connector.

First, let’s create a store object. We can do that by instantiating the store class provided by the redux package. When creating the store object, we need to specify the type of the state property (the class of our state object) as a generic type parameter. And then, pass the reducer function, and the initial state as arguments. We can get the initial state by calling the initial constructor of our state class.

Now that we have a store object, we need to pass it down the widget tree. The flutter redux package provides us with a StoreProvider that would pass our store object down the widget tree. All that we need to do is to wrap our root widget with the StoreProvider. Mention MaterialApp as the child and assign our store object to the store parameter.

class MyApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    final Store<AppState> store =
        new Store<AppState>(appStateReducer, initialState: AppState.initial());
    return StoreProvider<AppState>(
        store: store,
        child: MaterialApp(
          title: 'Flutter Demo',
          theme: ThemeData(
            // This is the theme of your application.
            //
            // Try running your application with "flutter run". You'll see the
            // application has a blue toolbar. Then, without quitting the app, try
            // changing the primarySwatch below to Colors.green and then invoke
            // "hot reload" (press "r" in the console where you ran "flutter run",
            // or simply save your changes to "hot reload" in a Flutter IDE).
            // Notice that the counter didn't reset back to zero; the application
            // is not restarted.
            primarySwatch: Colors.blue,
          ),
          home: MyHomePage(title: 'Flutter Demo Home Page'),
        ));
  }
}

Accessing the Redux store from the Flutter Widgets

Now, we can access the store object anywhere in our app using the StoreConnector. First, let’s display the counter value.

In the _MyHomePageState widget, let’s assign the StoreConnector widget to the body parameter. Specify the AppState class and ViewModel class as the generic parameters. This StoreConnector widget has two properties: connector and builder. The connector accepts a function that accepts a store object as an argument and returns a view model object. We can create a ViewModel object by using the factory constructor. The builder parameter accepts a function that takes in a BuildContext object and a ViewModel object as arguments and returns a widget.

We can use the viewModel argument to display the counter value. Remember, the viewModel object has a property called counter that stores the counter value. We can display the counter values using viewModel.counter.toString().

To dispatch an action, we can use viewModel.onIncrement() method. Assign it to the onPressed parameter of the floating action button.

class _MyHomePageState extends State<MyHomePage> {
  int _counter = 0;
 
  void _incrementCounter() {
    setState(() {
      _counter++;
    });
  }
 
  @override
  Widget build(BuildContext context) {
   
    return Scaffold(
        appBar: AppBar(
        
          title: Text(widget.title),
        ),
        body: StoreConnector<AppState, ViewModel>(
          converter: (Store<AppState> store) => ViewModel.create(store),
          builder: (BuildContext context, ViewModel viewModel) => Center(
            
              mainAxisAlignment: MainAxisAlignment.center,
              children: <Widget>[
                Text(
                  'You have pushed the button this many times:',
                ),
                Text(
                  viewModel.count.toString(),
                  style: Theme.of(context).textTheme.display1,
                ),
              ],
            ),
          )),
          floatingActionButton: StoreConnector<AppState, ViewModel>(
            converter:(Store<AppState> store)=>ViewModel.create(store),
            builder:(BuildContext context, ViewModel viewModel)=>
          FloatingActionButton(
            onPressed: ()=>viewModel.onIncrement(),
            tooltip: 'Increment',
            child: Icon(Icons.add),
          ), // This trailing comma makes auto-formatting nicer for build methods.
        ));
  }
}

When you click on the floating action button, it calls the onIncrement method of the viewModel object. This method increments the counter value by one and passes it into the IncrementAction class constructor. This calls the reducer and passes the state and the created object of the IncrementAction class as the arguments. The reducer then takes the new counter value from the action object and creates a Counter object with the new counter value. This object is then passed into the AppState constructor to create a new state object which is returned by the reducer function. As the new state object is returned, the viewModel’s counter property gets updated and the view is updated with the new counter value.

A screenshot of a cell phone
Description automatically generated
The default Flutter demo app modified to function using Redux

There it is! We have connected our state built using Redux to the view of our Futter app. Now, we have a single source of truth and don’t need to worry about passing data among components.

The full source code can be found here: https://github.com/thivi/FlutterReduxSample

Leave a Reply

placeholder="comment">