In this blog post, we will look more deeply into state manage in flutter apps.
App State
What exactly is state?
For any application we develop there is always data involved. In short data used by the app is called state. App state can be of different types like UI state (e.g showing a loading indicator, value of a dropdown, value of text field, to show/hide a element, etc). App state is also the data which is needed in an app to display UI e.g logged in user information, maybe a list we display from our api response or any such data.
So app state is basically all data/information we need to display the current state of app. Don’t not confuse this with database or data stored in a db. A database contain’s entire collection of data, while app state only has data to display the current ui for the app.
Problem Statement
Let’s first try to understand what we are trying to solve. So what exactly is state management?
Let’s say we have an app with different widgets/component or elements. We need to be able to define a global state, we need to be able to communicate state changes between widgets so then can react accordingly. There should be a consistent way of doing this so that we can also keep track of app state and easily debug if something goes wrong.
So there are two main entities in any app.
Data <=====> UI
Any change in data should trigger an ui update, and ui will update data which will again trigger UI update.
State management is basically to optimize this cycle for apps, make it consistent, scalable and easy to use.
Let’s see where we are currently in terms of state management in our flutter app with an example.
Filters (dropdown)
In the previous blog we saw that we were able to display a list of data. Now i want to add a filter to it, i.e to be able to filter data. To be specific i have two filters a) Category b) Sub category. Selecting the first filter i.e Category will load data to the second filter i.e Sub Category and this will further filter the data. Also both these filter values will come from api’s
I will be using the ” DropdownButton ” widget for this.
So let’s first setup our services
static Future<List<String>> getFundCategoryFilter() async {
final response = await http
.get("http://176.9.137.77:8342/api/funds/category?format=json");
if (response.statusCode == 200) {
// If server returns an OK response, parse the JSON.
List filters = json.decode(response.body);
return filters.map((ele) => ele.toString()).toList();
} else {
// If that response was not OK, throw an error.
throw Exception('Failed to load post');
}
}
static Future<List<String>> getFundSubCategoryFilter(String category) async {
final response = await http.get(
"http://176.9.137.77:8342/api/funds/subcategory/$category?format=json");
if (response.statusCode == 200) {
// If server returns an OK response, parse the JSON.
List filters = json.decode(response.body);
return filters.map((ele) => ele.toString()).toList();
} else {
// If that response was not OK, throw an error.
throw Exception('Failed to load post');
}
}
It’s a standard code same as before. We don’t need to make any custom model since the api in current just returns an array of strings.
Next, lets look at our dropdown. First we need to make a statefull widget
class FundFilterType extends StatefulWidget {
@override
_FundFilterTypeState createState() => new _FundFilterTypeState();
}
and our actual widget state looks like this
class _FundFilterTypeState extends State<FundFilterType> {
String categoryFilter;
String subFilter;
@override
Widget build(BuildContext context) {
return Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Flexible(
child: FutureBuilder(
future: AMCService.getFundCategoryFilter(),
builder:
(BuildContext context, AsyncSnapshot<List<String>> snapshot) {
if (snapshot.hasData) {
return DropdownButton<String>(
value: categoryFilter,
icon: Icon(Icons.arrow_downward),
iconSize: 24,
elevation: 16,
style: TextStyle(color: Colors.deepPurple),
underline: Container(
height: 2,
color: Colors.deepPurpleAccent,
),
onChanged: (String newValue) {
setState(() {
categoryFilter = newValue;
});
},
items: snapshot.data
.map<DropdownMenuItem<String>>((String value) {
return DropdownMenuItem<String>(
value: value,
child: Text(value),
);
}).toList(),
);
} else {
return CircularProgressIndicator();
}
},
)),
Flexible(
child: categoryFilter != null
? FutureBuilder(
future: AMCService.getFundSubCategoryFilter(categoryFilter),
builder: (BuildContext context,
AsyncSnapshot<List<String>> snapshot) {
if (snapshot.hasData) {
return DropdownButton<String>(
value: subFilter,
icon: Icon(Icons.arrow_downward),
iconSize: 24,
elevation: 16,
style: TextStyle(color: Colors.deepPurple),
underline: Container(
height: 2,
color: Colors.deepPurpleAccent,
),
onChanged: (String newValue) {
setState(() {
subFilter = newValue;
});
},
items: snapshot.data
.map<DropdownMenuItem<String>>((String value) {
return DropdownMenuItem<String>(
value: value,
child: Text(value),
);
}).toList(),
);
} else {
return CircularProgressIndicator();
}
},
)
: Container(),
),
Flexible(
child: FlatButton(
child: Text("Reset"),
onPressed: () {
setState(() {
categoryFilter = null;
subFilter = null;
});
},
),
)
],
);
}
}
Once you deploy this you would see Two dropdown, working with each other.
But let’s pause at this stage a bit. There seem to be few issues with this code especially with managing state i.e values of category, sub category
a) If we want to use our previous widget which we created to display list data, we cannot pass these variables to that easily. Meaning sending data across widget’s is not possible at this stage.
b) Right now we are just looking a local state variable for widgets. What about global state like login data i.e if we wish to display user’s logged in data in some widget.
There are many more issues, we need a better state management solution for for our apps, if we are build bigger apps. Flutter comes with many different options for this https://flutter.dev/docs/development/data-and-backend/state-mgmt/options
But we will look at Redux. The reason being its the most widely used state management architecture across different frameworks.
Flux
Redux is a library based on “Flux” architecture for state management!
There are lot of articles online on flux, redux which you must read to understand basic concepts of it. Its very important. This is official guide on flux https://facebook.github.io/flux/
Redux
Go through this https://redux.js.org/introduction/motivation but from a javascript background but good to learn concepts.
also go through this article https://blog.novoda.com/introduction-to-redux-in-flutter/ which explains basics of redux well. Also this is also a very good explanation of redux . There are many blogs available to explain concepts of Redux as it’s very widely used and its not specific to flutter.
Let me list down here few key concepts which you need to know in short
State
There should be a single unified “state” for the app. This could be a class which has data. We cannot have multiple state, we need to a single global state.
Actions
Action are functions which ui can trigger to update the state. The only way to update “state” is by calling action. You should never try to directly modify app state.
Reducers
Reducer are function which actually make changes to state data. These need to be “pure” function which means, if we call the same reducer again with same state, it should always return the same response.
These are the basic principle of Redux https://redux.js.org/introduction/three-principles
Redux In Action
Let’s integrate redux to our app and see these same principles in action.
https://pub.dev/packages/flutter_redux#-installing-tab-
First install redux using above link.
What we will implement is a simple counter i.e to just to hold value count like 0,1,2,3,4.
First we need to design our app state in our case is just a an integrate.
int appState = 0;
Next, let’s create an action . Action is simply a trigger or identifier which informs redux that a state update needs to be done.
class IncrementAction {
IncrementAction();
}
//FYI, this can be an enum as well.
Next, lets define a reducer. Reducer is a function which actually updates (or mutates) the app state.
int reducer(int state, dynamic action) {
if (action is IncrementAction) {
state = state + 1;
}
return state;
}
So here based on action, we update the state. In our case just a simple increment.
Next, we need to connect to this to our flutter app. Just an FYI, the above 3 steps we didn’t are not specific to flutter at all, rather general concepts of redux valid across frameworks.
void main() {
final store = new Store<int>(reducer, initialState: appState);
runApp(MainApp(store));
}
Ok, so here we use flutter store to connection “reducer” and “appState” together.
class MainApp extends StatelessWidget {
final Store<int> store;
MainApp(this.store);
@override
Widget build(BuildContext context) {
return StoreProvider<int>(
store: store,
child: MaterialApp(
title: 'My First Flutter Redux App',
theme: ThemeData.light(),
home: Scaffold(
appBar: AppBar(
title: Text('Redux Counter'),
),
body: Center(
child: TestDisplayWidget(),
),
floatingActionButton: TestActionWidget()
)
)
);
}
}
Next, our MainApp is connect to MaterialApp using StoreProvider
Next, lets look at TestDisplayWidget
class TestDisplayWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return new StoreConnector<int, String>(
converter: (store) => store.state.toString(),
builder: (context, counter) {
return Text("Counter value $counter");
});
}
}
Ok, so this is section is important. Here we are using “StoreConnector” widget to connect Redux store with a UI widget.
This connects app state to widget.
here we have a “converter” which takes value from a state and render UI which we want. What is most awesome thing about this is that its reactive! means, if state updates the ui updates automatically!
Next, lets see our action
class TestActionWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return new StoreConnector<int, VoidCallback>(converter: (store) {
return () => store.dispatch(IncrementAction());
}, builder: (context, callback) {
return FloatingActionButton(child: Icon(Icons.plus_one), onPressed: callback);
});
}
}
So again we use StoreConnector to connect flutter with UI but this time we dispatch the action.
One more thing to note is
StoreConnector<int, VoidCallback>
the first arg is store type and second the return from converter function. since dart is strongly typed this is important to note.
This is how my full code looks now
import 'package:flutter/material.dart';
import 'package:flutter_redux/flutter_redux.dart';
import 'package:redux/redux.dart';
//this is a single class or can be a string. mainly just to identify action
class IncrementAction {
IncrementAction();
}
// this is a reduce which changes the state depending on action
int reducer(int state, dynamic action) {
if (action is IncrementAction) {
state = state + 1;
}
return state;
}
int appState = 0;
void main() {
final store = new Store<int>(reducer, initialState: appState);
runApp(MainApp(store));
}
class MainApp extends StatelessWidget {
final Store<int> store;
MainApp(this.store);
@override
Widget build(BuildContext context) {
return StoreProvider<int>(
store: store,
child: MaterialApp(
title: 'My First Flutter Redux App',
theme: ThemeData.light(),
home: Scaffold(
appBar: AppBar(
title: Text('Redux Counter'),
),
body: Center(
child: TestDisplayWidget(),
),
floatingActionButton: TestActionWidget()
)
)
);
}
}
class TestDisplayWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return new StoreConnector<int, String>(
converter: (store) => store.state.toString(),
builder: (context, counter) {
return Text("Counter value $counter");
});
}
}
class TestActionWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return new StoreConnector<int, VoidCallback>(converter: (store) {
return () => store.dispatch(IncrementAction());
}, builder: (context, callback) {
return FloatingActionButton(child: Icon(Icons.plus_one), onPressed: callback);
});
}
}