-
Notifications
You must be signed in to change notification settings - Fork 35
210 State Management Alternatives
Page Table of Contents
Other than many mobile development frameworks, Flutter [1] does not impose any kind of architecture or State Management solution on its developers. This open-ended approach has lead to multiple State Management solution and a hand full of architectural approaches spawning from the community [52]. Some of these approaches have even been endorsed by the Flutter Team itself [12]. I decided to focus on the BLoC pattern [7] for this Guide. But I do want to showcase some alternatives and explain why exactly I ended up choosing BLoC.
I will showcase the State Management solutions using one example of App State from the Wisgen App [11]. We have a list of favorite wisdoms in the Wisgen App. This State is needed by 2 parties:
- The ListView on the favorite page, so it can display all favorites
- The button on every wisdom card so it can add a new favorite to the list and show if a given wisdom is a favorite.
Figure 13: Wisgen Favorites [11]
So whenever the favorite button on any card is pressed, several Widgets [28] have to update. This is a simplified version of the Wisgen Widget Tree, the red highlights show the Widgets that need access to the favorite list, the heart shows a possible location from where a new favorite could be added.
Figure 14: Wisgen WidgetTree Favorites [11]
The Provider Package [53] is an open-source package for Flutter developed by Remi Rousselet in 2018. It has since then been endorsed by the Flutter Team on multiple occasions [54], [55] and they are now devolving it in cooperation. The package is basically a prettier interface for Inherited Widgets [36]. You can use Provider to expose State from a Widget at the top of the tree to any number of Widgets below it in the tree.
As a quick reminder: Data in Flutter always flows downwards. If you want to access data from multiple locations within your Widget Tree, you have to place it at one of their common ancestors so they can both access it through their build contexts. This practice is called “lifting State up” and it is a common practice within declarative frameworks [56].
📙 | Lifting State up | Placing State at the lowest common ancestor of all Widgets that need access to it [56] |
---|
The Provider Package is an easy way for us to lift State up. Let’s look at our example from figure 14: The first common ancestor of all Widgets in need of the favorite list is MaterialApp. So we will need to lift the State up to the MaterialApp and then have our Widgets access it from there:
Figure 15: Wisgen WidgetTree Favorites with Provider [11]
To minimize re-builds the Provider Package uses ChangeNotifiers [57]. This way Widgets can subscribe/listen to the Sate and get notified whenever the State changes. This is how an implementation of Wisgen’s favorite list would look like using Provider: Favorites is the class we will use to provide our favorite list globally. The notifyListeners() function will trigger rebuilds on all Widgets that listen to it.
class Favorites with ChangeNotifier{
//State
final List<Wisdom> _wisdoms = new List();
add(Wisdom w){
_wisdoms.add(w);
notifyListeners(); //Re-Build all Listeners
}
remove(Wisdom w){
_wisdoms.remove(w);
notifyListeners(); //Re-Build all Listeners
}
bool contains(Wisdom w) => _wisdoms.contains(w);
}
Code Snippet 22: Hypothetical Favorites Class that would be exposed through the Provider Package [11]
Here we expose our Favorite class globally above MaterialApp in the WidgetTree using the ChangeNotifierProvider Widget:
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
//Providing Favorites Globally
return ChangeNotifierProvider(
builder: (_) => Favorites(),
child: MaterialApp(home: WisdomFeed()),
);
}
}
Code Snippet 23: Providing Favorites Globally [11]
This is how listening to the Favorite class looks like. We use the Consumer Widget to get access to the favorite list and everything below the Consumer Widget will be rebuild when the favorites list changes.
...
@override
Widget build(BuildContext context) {
return Expanded(
flex: 1,
child: Consumer<Favorites>( //Consuming Global instance of Favorites
builder: (context, favorites, child) => IconButton(
//Display Icon Button depending on current State
icon: Icon(favorites.contains(wisdom)
? Icons.favorite
: Icons.favorite_border),
color: favorites.contains(wisdom)
? Colors.red
: Colors.grey,
onPressed: () {
//Add/remove Wisdom to/from Favorites
if (favorites.contains(wisdom)) favorites.remove(wisdom);
else favorites.add(wisdom);
},
),
),
)
}
...
Code Snippet 24: Consuming Provider in Favorite Button of Wisdom Card [11]
All in all, Provider is a great and easy solution to distribute State in a small Flutter application. But it is just that, a State Management solution and not an architecture [54], [55], [58], [59]. Just the Provider package alone with no pattern to follow or an architecture to obey will not lead to a clean and manageable application. But no worries, I did not teach you about the package for nothing. Because Provider is such an efficient and easy way to distribute State, the BLoC package [37] uses it as an underlying technology for their approach.
Redux [60] is an Architectural Pattern with a State Management solution. It was originally built for React [20] in 2015 by Dan Abramov. It was late ported to Flutter by Brian Egan in 2017 [61]. In Redux, we use a Store as one central location for all our Business Logic. This Store is put at the very top of our Widget Tree and then globally provided to all Widgets using an Inherited Widget. We extract as much logic from the UI as possible. It should only send actions to the store (such as user input) and display the interface dependant on the Current State of the Store. The Store has reducer functions, that take in the previous State and an action and return a new State. [56], [58], [62] So in Wisgen, the Dataflow would look something like this:
Figure 16: Wisgen Favorite List with Redux [11]
Our possible actions are adding a new wisdom and removing a wisdom. So this is what our Action classes would look like:
@immutable
abstract class FavoriteAction {
//Wisdom related to action
final Wisdom _favorite;
get favorite => _favorite;
FavoriteAction(this._favorite);
}
class AddFavoriteAction extends FavoriteAction {
AddFavoriteAction(Wisdom favorite) : super(favorite);
}
class RemoveFavoriteAction extends FavoriteAction {
RemoveFavoriteAction(Wisdom favorite) : super(favorite);
}
Code Snippet 25: Hypothetical Wisgen Redux Actions [11]
This what the reducer function would look like:
List<Wisdom> favoriteReducer(List<Wisdom> state, FavoriteAction action) {
if (action is AddFavoriteAction) state.add(action.favorite);
if (action is RemoveFavoriteAction) state.remove(action.favorite);
return state;
}
Code Snippet 26: Hypothetical Wisgen Redux Reducer [11]
And this is how you would make the Store globally available:
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
//Create new Store from reducer function
favoriteStore = new Store<List<Wisdom>>(favoriteReducer, initialState: new List());
//Provide Store globally
return StoreProvider<List<Wisdom>>((
store: favoriteStore,
child: MaterialApp(home: WisdomFeed()),
);
}
}
Code Snippet 27: Providing Redux Store globally in Wisgen [11]
Now the Favorite button from snippet 24 would be implemented like this:
...
@override
Widget build(BuildContext context) {
return Expanded(
flex: 1,
child: StoreConnector( //Consume Store
converter: (store) => store.state, //No need for conversion, just need current State
builder: (context, favorites) => IconButton(
//Display Icon Button depending on current State
icon: Icon(favorites.contains(wisdom)
? Icons.favorite
: Icons.favorite_border),
color: favorites.contains(wisdom)
? Colors.red
: Colors.grey,
onPressed: () {
//Add/remove Wisdom to/from Favorites
if (favorites.contains(wisdom)) store.dispatch(AddFavoriteAction(wisdom));
else store.dispatch(RemoveFavoriteAction(wisdom));
},
),
),
)
}
...
Code Snippet 28: Consuming Redux Store in Favorite Button of Wisdom Card [11]
I went back and forth on this decision a lot. Redux is a great State Management solution with some clear guidelines on how to integrate it into a Reactive application [63]. It also enables the implementation of a clean four-layered architecture (View - Store - Data) [56]. Didier Boelens recommends to just stick to a Redux architecture if you are already familiar with its approach from other cross-platform development frameworks like React [20] and Angular [64] and I very much agree with this advice [58]. I have previously never worked with Redux and I decided to use BLoC over Redux because:
- It was publicly endorsed by the Flutter Team on multiple occasions [7], [12], [50], [54], [65]
- It also has clear architectural rules [7]
- It also enables the implementation of a clean four-layered architecture [66]
- It was developed by one of Flutter’s Engineers [7]
- We don’t end up with one giant store for the business logic out with multiple blocs with separate responsibilities [58]
This Guide is licensed under the Creative Commons License (Attribution-NoDerivatives 4.0 International)
Author: Sebastian Faust.