Reputation: 30176
I'm currently reading the example code of the provider package:
// ignore_for_file: public_member_api_docs
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
void main() => runApp(MyApp());
class Counter with ChangeNotifier {
int _count = 0;
int get count => _count;
void increment() {
_count++;
notifyListeners();
}
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MultiProvider(
providers: [
ChangeNotifierProvider(builder: (_) => Counter()),
],
child: Consumer<Counter>(
builder: (context, counter, _) {
return MaterialApp(
supportedLocales: const [Locale('en')],
localizationsDelegates: [
DefaultMaterialLocalizations.delegate,
DefaultWidgetsLocalizations.delegate,
_ExampleLocalizationsDelegate(counter.count),
],
home: const MyHomePage(),
);
},
),
);
}
}
class ExampleLocalizations {
static ExampleLocalizations of(BuildContext context) =>
Localizations.of<ExampleLocalizations>(context, ExampleLocalizations);
const ExampleLocalizations(this._count);
final int _count;
String get title => 'Tapped $_count times';
}
class _ExampleLocalizationsDelegate
extends LocalizationsDelegate<ExampleLocalizations> {
const _ExampleLocalizationsDelegate(this.count);
final int count;
@override
bool isSupported(Locale locale) => locale.languageCode == 'en';
@override
Future<ExampleLocalizations> load(Locale locale) =>
SynchronousFuture(ExampleLocalizations(count));
@override
bool shouldReload(_ExampleLocalizationsDelegate old) => old.count != count;
}
class MyHomePage extends StatelessWidget {
const MyHomePage({Key key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Title()),
body: const Center(child: CounterLabel()),
floatingActionButton: const IncrementCounterButton(),
);
}
}
class IncrementCounterButton extends StatelessWidget {
const IncrementCounterButton({Key key}) : super(key: key);
@override
Widget build(BuildContext context) {
return FloatingActionButton(
onPressed: Provider.of<Counter>(context).increment,
tooltip: 'Increment',
child: const Icon(Icons.add),
);
}
}
class CounterLabel extends StatelessWidget {
const CounterLabel({Key key}) : super(key: key);
@override
Widget build(BuildContext context) {
final counter = Provider.of<Counter>(context);
return Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
const Text(
'You have pushed the button this many times:',
),
Text(
'${counter.count}',
style: Theme.of(context).textTheme.display1,
),
],
);
}
}
class Title extends StatelessWidget {
const Title({Key key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Text(ExampleLocalizations.of(context).title);
}
}
At first I was confused to see the following code. It is a MultiProvider, immediately followed by a Consumer, at the top of the Widget tree:
return MultiProvider(
providers: [
ChangeNotifierProvider(builder: (_)=>Counter()),
],
child: Consumer<Counter>(
builder: (context, counter, _){
return MaterialApp(
home: const MyHomePage()
);
},
),
);
I was wondering: Isn't this really bad for performance?
Everytime the state of the consumer is updated, all the tree has to be rebuilt.
Then I realized the const
qualifiers everywhere. This seems like a very neat setup.
I decided to debug through it and see when and where widgets are rebuilt.
When the app is first started, flutter goes down the tree and builds the widgets one by one. This makes sense.
When the button is clicked and the Counter
is incremented, builder
is called on the Consumer at the very top of the tree. After that, build
is called on CounterLabel
and IncrementCounterButton
.
CounterLabel
makes sense. This is not const
and will actually change its content.
But IncrementCounterButton
is marked as const
. Why does it rebuild?
It is not clear to me why some const
widgets are rebuilt while others aren't. What is the system behind this?
Upvotes: 6
Views: 2408
Reputation: 277527
The most common reasons for a widget to rebuild are:
Const instance of widgets are immune to the first reason, but they are still affected by the two others.
This means that a const instance of a StatelessWidget will rebuild only if one of the inherited widget it uses update.
Upvotes: 13
Reputation: 2507
Building on @RayLi and @Remi's answers, another way to prevent a rebuild is to make this modification:
// onPressed: Provider.of<Counter>(context).increment, // This listens
onPressed: context.read<Counter>().increment, // this doesn't listen
context.read()
won't update, but in this case this is what you want. onPressed
will be mapped to the same instance of .increment
throughout the FloatingActionButton's existence.
context.read<Counter>()
has the same behavior as Provider.of<Counter>(context, listen: false)
. See Is Provider.of(context, listen: false) equivalent to context.read()?
Upvotes: 0
Reputation: 7919
Provider is a convenient wrapper for InheritedWidget with a lot of nice things done for you.
Because IncrementCounterButton
accesses Provider (and InheritedWidget under the hood), it listens and rebuilds whenever the data changes.
To prevent buttons or other widgets that do not need to be rebuild on data change, set listen
to false
.
Provider.of(context, listen: false).increment
The caveat is that if the root widget rebuilds, widgets marked with listen: false
will still rebuild. Understand how listen: false works when used with Provider<SomeType>.of(context, listen: false)
Hope this helps!
Upvotes: 1