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,
- The state
- Actions
- Dispatcher
- Reducer
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.
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.
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