When it comes to state management in Flutter, most of the time people prefer to use Provider. A package that is most liked in pub.dev, was created by Remi, and is recommended by Flutter team. Provider is in its version 4 now and had a lot of changes as well as improvements in the past. There actually are so many posts and articles that you can find on the Internet on how to use this package. So in this post, I’m not going to iterate that loop.
source code of this post can be found here: github
The use of Consumer:
If you’ve been using Provider you probably know why and how to use Consumer. Basically, it’s the way to narrow down the widget you need to rebuild on each state update. It is ideal to use Consumer as deep in your widget tree as possible so that reduces the amount of unnecessary re-render.
Put into the context:
What if you have more than 1 thing that could be changed in your provider? That means when one value changes and notifyListeners() is called all widgets that listen to the other values in the same ChangeNotifier will be triggered to rebuild as well. And Consumer doesn’t help in this case since it listens to a whole Provider, not a particular value.
Let’s examine the code of a provider class below:
class NumberProvider extends ChangeNotifier {
int _number1 = 0;
int _number2 = 1;
int get number1 => _number1;
int get number2 => _number2;
void add() {
_number1++;
_number2++;
notifyListeners();
}
void addTo1() {
_number1++;
notifyListeners();
}
void addTo2() {
_number2++;
notifyListeners();
}
}
And here a screen that displays number 1 and 2
class ConsummerScreen extends StatelessWidget {
const ConsummerScreen({Key key}) : super(key: key);
@override
Widget build(BuildContext context) {
print('build home');
return Container(
child: Scaffold(
floatingActionButton: Row(
mainAxisSize: MainAxisSize.min,
children: [
FloatingActionButton(
heroTag: 'all',
onPressed: () {
context.read<NumberProvider>().add();
},
child: Text('all'),
),
FloatingActionButton(
heroTag: '1',
onPressed: () {
context.read<NumberProvider>().addTo1();
},
child: Text('1'),
),
FloatingActionButton(
heroTag: '2',
onPressed: () {
context.read<NumberProvider>().addTo2();
},
child: Text('2'),
),
],
),
body: SafeArea(
child: Center(
child: Container(
child: Consumer<NumberProvider>(
builder: (context, provider, child) {
print('rebuild consumer');
return Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
color: Colors.red,
padding: EdgeInsets.all(10),
child: Text('${provider.number1}'),
),
Container(
color: Colors.green,
padding: EdgeInsets.all(10),
child: Text('${provider.number2}'),
),
],
);
},
),
),
),
),
),
);
}
}
I created 3 methods in the provider class that will update both numbers simultaneously or separately. But since the screen using Consumer and listens NumberProvider, it doesn’t matter which button we tap on. The widgets that display number1 and number2 both get rebuilt. In a large scale application, this leads to a waste of resource.
Selector for good:
Selector is a class in Provider package that is less known compare to Consumer yet useful. As the name suggests, Selector allows you to select a specific value in a Provider to listen to. Then when and only when that selected value changes, the widget that returns by the builder method of Selector will rebuild.
This is done by the selector function will compare the previous value and the new value whenever the provider notifies something. If the selected value is changed, then the builder method is triggered. Pretty handy eh?
Have a look at the code below:
Container(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Selector<NumberProvider, int>(
selector: (_, provider) => provider.number1,
builder: (context, number1, child) {
print('Build num1');
return Container(
color: Colors.red,
padding: EdgeInsets.all(10),
child: Text('$number1'),
);
}),
Selector<NumberProvider, int>(
selector: (_, provider) => provider.number2,
builder: (context, number2, child) {
print('Build num2');
return Container(
color: Colors.green,
padding: EdgeInsets.all(10),
child: Text('$number2'),
);
}),
],
),
),
Instead of using one Consumer, I used 2 Selectors for each number. Therefore, if number1 gets updated, only the respective widget rebuilds, the other widget that displays number2 stays still.
Addition to selector
There are some variations of Selector that you can use to listen to more than one selected value such as: Selector1, Selector2,…
But there is another way you can do that using the Tuple package and is recommended by the author of Provider:
Selector<Foo, Tuple2<Bar, Baz>>(
selector: (_, foo) => Tuple2(foo.bar, foo.baz),
builder: (_, data, __) {
return Text('${data.item1} ${data.item2}');
}
)
Conclusion
Selector class helps you narrow down the scope of widgets that needs to be rebuilt and easier to manage what the widget is listening to. Make good use of this class and save your app some redundant rebuilds
Awesome explanation brother
Thank you!!
According to provider documentation, context.read should not be used inside build methods?
In my example. it’s used inside methods. Wich is recommended, since .read doesn’t trigger rebuild.
You can take a look at: https://stackoverflow.com/a/62434596/9145277
By the way. I ditched `Provider` only `riverpod` now