Flutter State Management with Provider — NotifyListeners vs StreamProvider — Part 1

Wilker Oliveira
Flutter Community
Published in
5 min readDec 15, 2019
Source: https://devdojo.com/articles/be-a-part-of-the-extensive-bootcamp-for-the-flutter

**** Updated December 25 to add the Selector widget. ****

When working with Flutter sooner or later we will need a state management solution and choosing setState may not be the best approach.

We can use other approaches like Redux, MobX and of course BloC (with or without Rx).

Today, however, we will use another state management, the Provider .

As the documentation explains:

“A mixture between dependency injection (DI) and state management, built with widgets for widgets”

In this tutorial, I will show you two ways we can use the Provider and the difference between them; however, I will not explain in detail about the Provider and its classes. For this you can read the excellent Provider documentation here.

But to work with it, we need to configure our project first, adding the Provider reference in pubspec.yaml file:

dependencies: 
provider: ^3.2.0
Source: https://giphy.com/

Simple app state management — ChangeNotifierProvider

In other words, ChangeNotifierProvider (among other things) provides a way that you can instantiate a class that extends ChangeNotifier (DI) and through the Consumer, the descendants widgets will be notified and rebuilt.

But ChangeNotifierProvider instantiates only one class, and if you have more than one class that extends ChangeNotifier, you need to use MultiProvider instead of ChangeNotifierProvider.

Remember that you need to avoid placing the ChangeNotifierProvider higher than necessary in the widget tree, otherwise you will pollute the scope.

These codes below are just examples showing how to use ChangeNotifierProvider and MultiProvider.

Using ChangeNotifierProvider:

void main() => runApp(MyApp());class MyApp extends StatelessWidget {   @override   Widget build(BuildContext context) {    return ChangeNotifierProvider(     create: (context) => CreditCardViewModel(),      child: MaterialApp(       title: 'Simple Provider App',        theme: ThemeData(primarySwatch: Colors.blue,
),
home: MainScreen(), ), ); }}

Using MultiProvider:

void main() => runApp(MyApp());class MyApp extends StatelessWidget {   @override   Widget build(BuildContext context) {    return MultiProvider(     providers: [      ChangeNotifierProvider(create: (context) =>          CreditCardViewModel()),       Provider<CreditCardStreamViewModel>.value(

value: CreditCardStreamViewModel(),
), ], child: MaterialApp( title: ‘Simple Provider App’, theme: ThemeData(primarySwatch: Colors.blue,
),
home: MainScreen(), ), ); }}

Consumer

Consumer is responsible for consuming a type that extends ChangeNotifier.

We have to use the builder function, which will be executed when the type we’re listening to, in the Consumer widget, calls the notifyListeners function.

To use this widget we must set the type that we want to access:

Consumer<CreditCardViewModel>

The full implementation of this widget is:

Consumer<CreditCardViewModel>(   builder: (context, model, child) => Container(    child: Container(),   ),);

NotifyListeners

NotifyListeners is the easiest and simplest way to work with the Provider where we just need to call the notifyListeners() method to tell the “listeners” to rebuild and get the new value.

To do this, you just need to extend ChangeNotifier and you are ready to go.

class CreditCardViewModel extends ChangeNotifier {   addExpense(String expenses) {    current.expenses.add(Expense(expenses));    notifyListeners();   }}

With this, all listeners that are listening to the CreditCardViewModel class will be notified and the widget will be rebuilt.

But remember, all the registered listeners will be called.

The example below shows this behavior when we have 3 dates: the First one that isn’t using a Consumer and the other two that are using the Consumer widget.

void main() => runApp(MyApp());class MyApp extends StatelessWidget {   @override   Widget build(BuildContext context) {    return ChangeNotifierProvider(     create: (context) => MyModel(),      child: MaterialApp(       title: ‘Provider with NotifyListeners’,         theme: ThemeData(primarySwatch: Colors.blue,       ),      home: MyHomePage(title: ‘Home Page’),    ),  ); }}class MyHomePage extends StatefulWidget {   MyHomePage({Key key, this.title}) : super(key: key);   final String title;   @override   _MyHomePageState createState() => _MyHomePageState();}class _MyHomePageState extends State<MyHomePage> {   @override   Widget build(BuildContext context) {    return Scaffold(     appBar: AppBar(      title: Text(widget.title),     ),     body: Center(      child: Column(        mainAxisAlignment: MainAxisAlignment.spaceEvenly,        children: <Widget>[          Text(‘Date Without Consumer :’ + DateTime.now().toString(),          ),          Consumer<MyModel>(           builder: (context, model, widget) => Column(            children: <Widget>[            Text(‘Second Current Date :’ + DateTime.now().toString(),
),
FlatButton( onPressed: () { model.updateSecond(); }, child: Text(“Increment Second: “ + model.second), ), ], ), ), Consumer<MyModel>( builder: (context, model, widget) => Column( children: <Widget>[ Text(‘Third Current Date :’ + DateTime.now().toString(),
),
FlatButton( onPressed: () { model.updateThird(); }, child: Text(“Increment Third: “ + model.third), ), ], ), ), ], ), ), ); }}class MyModel extends ChangeNotifier { int secondCount = 0; int thirdCount = 0; int value = 0; String second = “”; String third = “”; void updateSecond() { this.second = (secondCount++).toString(); notifyListeners(); } void updateThird() { this.third = (thirdCount++).toString(); notifyListeners(); } void changeValue() { this.value += 1; notifyListeners(); }}

When you press the button “Increment Second”, it calls “updateSecond” method from MyModel, updating the counter and then calling notifyListeners method. By doing this, all widgets inside the builder method, that are listening to MyModel class, including the Third Current Date, will be updated.

So, we must pay attention when working with notifyListeners and consumer, because if we have a big widget tree that is listening to the same type, it could be a problem.

What happens if you just want to listen to a specific value or variable? Let’s see!

In the example above, just add this code above the last Consumer:

Consumer<MyModel>(   builder: (context, model, child) => Column(    children: <Widget>[     Text(‘Listening a value :’ + model.value.toString(),
),
FlatButton( onPressed: () { model.changeValue(); }, child: Text(“Change Value”), ), ], ),),

Pressing the button “Change Value”, the value will be incremented by 1 and the Text will change, but notice that the other consumers have changed too.

Selector

Selector widget provides a way to listen a specific value and update our widgets when that value changes.

When the value changes, the builder function is executed and rebuilds our widget.

Selector has another function we need to use to set the value we want to listen, the selector function.

If we use the same example as the Consumer topic, we get something like this:

Selector<MyModel, String>(   builder: (context, value, child) => Column(    children: <Widget>[     Text('Second Current Date :' + DateTime.now().toString(),),    FlatButton(     onPressed: () {      myModel.updateSecond();     },    child: Text("Increment Second: " + value),   ),  ],),selector: (buildContext, model) => model.second,),

In the first line, we define the class we are listening to and the type of the value.

With the builder function we can get the updated value and use it in some widget.

In the selector function, we define the property we will listen.

The Selector will get a class instance using Provider.of, which means we need to instantiate our class using the Provider widget, for example:

class MyApp extends StatelessWidget {   @override   Widget build(BuildContext context) {    return ChangeNotifierProvider(     create: (context) => MyModel(),     child: MaterialApp(      title: 'Provider with NotifyListeners',       theme: ThemeData(primarySwatch: Colors.blue,),      home: MyHomePage(title: 'Home Page'),    ),   );  }}

Unlike the Consumer, the Selector widget will not rebuild all widgets, only the widget that is listening for the same property will be rebuilt.

If you change the implementation from Consumer to Selector, you will see that not all Dates will be updated when some value changes.

You can get a complete example using the Selector in the GitHub repository.

To learn more about Selector, check out the documentation.

Conclusion

As we have seen, using notifyListeners is the simplest way to work with state management, but we may have some problems if you have a large widget tree and choose the Consumer widget.

Using the Selector widget, you can listen to a specific property and rebuild only if that property changes.

But if you prefer to work with Stream, let’s look at it in Part 2 of this article.

--

--