lhk
lhk

Reputation: 30176

flutter: when are const widgets rebuilt?

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

Answers (3)

R&#233;mi Rousselet
R&#233;mi Rousselet

Reputation: 277527

The most common reasons for a widget to rebuild are:

  • It's parent rebuilt (whatever the reason is)
  • Element.markNeedsBuild have been called manually (typically using setState)
  • An inherited widget it depends on updated

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

chongman
chongman

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

Ray Li
Ray Li

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

Related Questions