0

In Dart 2, the keyword new and const are no longer needed to instantiate an object. For example, new ListView(...) is now equivalent to ListView(...).

However, some class constructors are declared const, including most Flutter widgets, like Text:

const Text(
    String this.data, {
    Key? key,
    this.style,
    // ...
    this.textHeightBehavior,
})

Then, to instantiate a Text, we can type either Text('hi!'), or const Text('hi!').

My question: Is there any difference in terms of performance or the compiled code between those two?

Note:

The usage of const at the call site is more prominent at certain places, for example in Android Studio, if we choose to "Wrap with Padding", the EdgeInsets.all constructor call uses const:

 Padding(
     padding: const EdgeInsets.all(8.0),
     child: Text('hi!'),
 ),
Randy Sugianto 'Yuku'
  • 71,383
  • 57
  • 178
  • 228
  • 1
    `const` is not automatic. You must use it explicitly use it to create compile-time constants. Also see https://stackoverflow.com/q/57607745/. – jamesdlin Jul 05 '21 at 10:35

1 Answers1

3

You should always try to use const when possible, especially if you're creating a Flutter app. Constant constructors are very important because they're used to "cache" widgets so that they won't be unnecessarily rebuilt.


Imagine you had list of 10k items in your Flutter app:

class Example extends StatelessWidget {
  const Example();

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        const Text('My items:'),
        
        ListView.builder(
          shrinkWrap: true,
          itemCount: 10000,
          itemBuilder: (_, i) => SomeComplexWidget(
            value: i,
          ),
        ),
      ],
    );
  }
}

There are a lot of factors that could trigger a widget rebuild but let's assume that you simply rotated your device screen a few times. With this code...

SizedBox(
  width: 100,
  height: 400,
  child: Example(),
),

... you aren't using a const constructor so you're rebuilding Example on every rebuild (= you're also unnecessarily rebuilding the Column, the ListView, and re-rendering a few SomeComplexWidget). ListView.builder is a lazy initializer so it will save you a bit but you still don't want it to always be rebuilt.

If you used a Column() or a plain ListView() constructor for your list, it would have affected performance even more (more jank and thus more skipped frames). If you did this instead...

SizedBox(
  width: 100,
  height: 400,
  child: const Example(),
),

... you would "cache" the Example widget (meaning that it will be built only once). As you can guess, this is a very simple but important performance optimization you can do. Using constant constructors is not just my word but it's officially recommended by the docs:

Note that if your widget contained something that depends on InheritedWidget, the const keyword wouldn't have effect (otherwise, the widget couldn't "listen" for changes on the inherited widget".


Bonus

In Flutter, you can also cache widgets "manually". When you cannot use const, you can use late (or late final) inside the state to cache the widget. Look at this example:

class OtherExample extends StatelessWidget {
  final List<Interests> interests;
  const OtherExample({
    required this.interests,
  });

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        const Text('My list'),
   
        MyInterestsList(
          data: interests,
        ),
      ]
    );
  }
}

You want MyInterestsList to be cached because it never changes but const doesn't work here (since interests is not a compile-time) constant. When you can't use const but you still want to cache a widget, do it manually:

class OtherExample extends StatefulWidget {
  final List<Interests> interests;
  const OtherExample({
    required this.interests,
  });

  @override
  _OtherExampleState createState() => _OtherExampleState();
}

class _OtherExampleState extends State<OtherExample> {
  late Widget myInterests = MyInterestsList(
     data: widget.interests,
  );

  @override
  void didUpdateWidget(OtherExample oldWidget) { 
    super.didUpdateWidget(oldWidget);

    // This makes sure that the interests list is updated ONLY when
    // it actually changes
    if (widget.interests != oldWidget.interests) {
      myInterests = MyInterestsList(
        data: widget.interests,
      );
    }
  }

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        const Text('My list'),
   
        myInterests,
      ]
    );
  }
}

Thanks to late you're caching your widget inside the state class. Thanks to the didUpdateWidget override, the MyInterestsList widget will only be rebuilt when the list changes. If the list never changes, MyInterestsList will never be rebuilt.

Alberto Miola
  • 4,643
  • 8
  • 35
  • 49
  • Thank you for the comprehensive answer! Also, TIL that `late` does a lazy initialization. (I came from Kotlin and thought that `late` is just equivalent to `lateinit` in Kotlin. Turned out that `late` can also mean *lazy* initialization.) – Randy Sugianto 'Yuku' Jul 07 '21 at 06:35