State management approaches
Detail Reference
1)InheritedWidget & InheritedModel
The InheritedWidget works as store by connecting to an StatefulWidget. So your StateContainer is really three classes:
class StateContainer extends StatefulWidget
class StateContainerState extends State<StateContainer>
class _InheritedStateContainer extends InheritedWidget
The InheritedWidget and the StateContainer are the simplest to set up, and once they’re set up they don’t change. The logic mainly lives in StateContainerState. Set up the first two:
class ShareDataWidget extends InheritedWidget {
ShareDataWidget({
@required this.data,
Widget child
}) :super(child: child);
final int data;
// Defines a convenient way for widgets in a subtree to get Shared data
static ShareDataWidget of(BuildContext context) {
return context.inheritFromWidgetOfExactType(ShareDataWidget);
}
@override
bool updateShouldNotify(ShareDataWidget old) {
return old.data != data;
}
}
class _TestWidget extends StatefulWidget {
@override
__TestWidgetState createState() => new __TestWidgetState();
}
This Widget is where all your state and logic can live. For this app, you’ll simply be able to store and manipulate your data.
class __TestWidgetState extends State<_TestWidget> {
@override
Widget build(BuildContext context) {
//use data shared in InheritedWidget
return Text(ShareDataWidget
.of(context)
.data
.toString());
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
print("Dependencies change");
}
}
Finally, we create a button that incremented the value of ShareDataWidget with each click:
class InheritedWidgetTestRoute extends StatefulWidget {
@override
_InheritedWidgetTestRouteState createState() => new _InheritedWidgetTestRouteState();
}
class _InheritedWidgetTestRouteState extends State<InheritedWidgetTestRoute> {
int count = 0;
@override
Widget build(BuildContext context) {
return Center(
child: ShareDataWidget(
data: count,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Padding(
padding: const EdgeInsets.only(bottom: 20.0),
child: _TestWidget(),
),
RaisedButton(
child: Text("Increment"),
onPressed: () => setState(() => ++count),
)
],
),
),
);
}
}
- Advantages:
- It can manage global state.
- Disadvantages:
- It can't manage local state.
- The UI logic and business logic are not separated.
- you can use top-down data flow to manage state, but child widgets can't change the data and notice father widget.
- It's hard to maintain when the app came to bigger.
2)Provider
Flutter has mechanisms for widgets to provide data and services to their descendants (in other words, not just their children, but any widgets below them). These mechanisms are just special kinds of widgets—InheritedWidget, InheritedNotifier, InheritedModel, and more.
With provider, you don’t need to worry about callbacks or InheritedWidgets. But you do need to understand 3 concepts:
- ChangeNotifier: It is a simple class included in the Flutter SDK which provides change notification to its listeners. In other words, if something is a ChangeNotifier, you can subscribe to its changes.
- ChangeNotifierProvider: It is the widget that provides an instance of a ChangeNotifier to its descendants. It comes from the provider package.
- Consumer
<a>Shopping cart example</a>
- Define an Item class to represent the Item information:
class Item {
Item(this.price, this.count);
double price;
int count;
}
- In our shopping app example, we want to manage the state of the cart in a ChangeNotifier. We create a new class that extends it, like so:
class CartModel extends ChangeNotifier {
/// Internal, private state of the cart.
final List<Item> _items = [];
/// An unmodifiable view of the items in the cart.
UnmodifiableListView<Item> get items => UnmodifiableListView(_items);
/// The current total price of all items (assuming all items cost $42).
int get totalPrice => _items.length * 42;
/// Adds [item] to cart. This and [removeAll] are the only ways to modify the
/// cart from the outside.
void add(Item item) {
_items.add(item);
// This call tells the widgets that are listening to this model to rebuild.
notifyListeners();
}
/// Removes all items from the cart.
void removeAll() {
_items.clear();
// This call tells the widgets that are listening to this model to rebuild.
notifyListeners();
}
}
ChangeNotifier is part of flutter:foundation and doesn’t depend on any higher-level classes in Flutter. It’s easily testable (you don’t even need to use widget testing for it). For example, here’s a simple unit test of CartModel:
test('adding item increases total cost', () {
final cart = CartModel();
final startingPrice = cart.totalPrice;
cart.addListener(() {
expect(cart.totalPrice, greaterThan(startingPrice));
});
cart.add(Item('Dash'));
});
You don’t want to place ChangeNotifierProvider higher than necessary (because you don’t want to pollute the scope). But in our case, the only widget that is on top of both MyCart and MyCatalog is MyApp.
void main() {
runApp(
ChangeNotifierProvider(
create: (context) => CartModel(),
child: MyApp(),
),
);
}
If you want to provide more than one class, you can use MultiProvider:
void main() {
runApp(
MultiProvider(
providers: [
ChangeNotifierProvider(create: (context) => CartModel()),
Provider(create: (context) => SomeOtherClass()),
],
child: MyApp(),
),
);
}
Now that CartModel is provided to widgets in our app through the ChangeNotifierProvider declaration at the top, we can start using it.
This is done through the Consumer widget.
return Consumer<CartModel>(
builder: (context, cart, child) {
return Text("Total price: ${cart.totalPrice}");
},
);
If you have a large widget subtree under your Consumer that doesn’t change when the model changes, you can construct it once and get it through the builder.
return Consumer<CartModel>(
builder: (context, cart, child) => Stack(
children: [
// Use SomeExpensiveWidget here, without rebuilding every time.
child,
Text("Total price: ${cart.totalPrice}"),
],
),
// Build the expensive widget here.
child: SomeExpensiveWidget(),
);
It is best practice to put your Consumer widgets as deep in the tree as possible. You don’t want to rebuild large portions of the UI just because some detail somewhere changed.
Provider.of<CartModel>(context, listen: false).removeAll();
Using the above line in a build method won’t cause this widget to rebuild when notifyListeners is called.
3)Scoped Model reference
A set of utilities that allow you to easily pass a data Model from a parent Widget down to it's descendants.