Published on

Do you know these facts about const in Flutter?

Authors
  • avatar
    Name
    Rosa Tiara
    Twitter

Overview

If you're a (Flutter) developer, you have probably seen and used const. Most developers think const just means "this value won't change". That's true, though, but in this blog we're gonna learn another interesting facts about const.

Actually, const performs compile-time canonicalization, which means that Dart creates only 1 instance of identical const objects across the app. Let's take a look at these examples:


Basic Example

class Point {
  final int x;
  final int y;
  
  const Point(this.x, this.y);
}

void main() {
  // ------- With const -------
  const point1 = Point(1, 2);
  const point2 = Point(1, 2);
  print(identical(point1, point2)); // true -> point1 and point2 stored as a same object
  print(identityHashCode(point1) == identityHashCode(point2)); // true
  
  // ------- Without const (using final) -------
  final point3 = Point(1, 2);
  final point4 = Point(1, 2);
  print(identical(point3, point4)); // false -> point3 and point2 are different objects
  print(identityHashCode(point3) == identityHashCode(point4)); // false
}

Go on! Copy the code into DartPad and try it yourself!

What's happening here?

  • point1 and point2 are the same object in memory because they are declared using const.
  • point3 and point4 are two separate objects, even though they have identical values, because they are declared using final.
  • Same memory address = same object

Compile-Time vs Runtime

const Compile-Time

const point1 = Point(1, 2);  // Evaluated at COMPILE TIME
const point2 = Point(1, 2);  // Compiler says: "I already made this!"

When you use const, the compiler evaluates these expressions before your app runs, sees they're identical, and creates just ` object that both variables reference, like the picture below.

points

There's only 1 Point(1, 2) object in memory, despite having two variables!

final Runtime

final point3 = Point(1, 2);  // Evaluated at runtime
final point4 = Point(1, 2);  // Evaluated again at runtime

When you use final, each time your app runs this code, it creates a new object in memory.

points

Now imagine if you have this code:

Column(
  children: [
    const SizedBox(height: 16),
    Text(someData),
    const SizedBox(height: 16),
    const Text('Fluuuuuttter'),
    const SizedBox(height: 16),
  ],
)

You might think there are 3 different SizedBox(height: 16) objects, right? But actually there's only 1 in memory, referenced three times.

Now if you have 1000 of these scattered across your app, that still counts as just 1 object, if you use const. Amazing, right?

What's actually happening behind the scene? Why is that possible?

When Flutter rebuilds your widget tree (which happens constantly), it does an identity check:

if (identical(oldWidget, newWidget)) {
  return; // Skip rebuild - nothing changed!
}

For const widgets, this check is instant. Flutter immediately knows that nothing changed and skips the entire rebuild process for that subtree.


Memory Savings

const is also amazing at memory savings. Let's try this code:

void main() {
  // 10,000 const references
  const singleConst = Point(99, 99);
  var constList = List.filled(10000, singleConst);
  
  var allSame = constList.every(
    (p) => identityHashCode(p) == identityHashCode(singleConst)
  );
  print('10,000 const Points - all same object? $allSame'); // true
  
  // 10,000 non-const references
  var nonConstList = List.generate(10000, (_) => Point(99, 99));
  var uniqueAddresses = nonConstList
    .map((p) => identityHashCode(p))
    .toSet();
  print('10,000 non-const Points - unique objects: ${uniqueAddresses.length}'); 
  // 10,000!
}

Based on the code above, here's the comparison between using const and not:

  • With const: 10,000 references → 1 object in memory
  • Without const: 10,000 references → 10,000 objects in memory

One thing that's also interesting in Flutter is that Strings get special treatment because Strings are always interened, regardless of const, final, or var.

// All of these are identical
const str1 = 'Hello';
final str2 = 'Hello';
var str3 = 'Hello';

print(identical(str1, str2)); // -> true
print(identical(str2, str3)); // -> true
print(identical(str1, str3)); // -> true

Dart automatically maintains a string pool. Every string literal 'Hello' in that code points to the same object in memory.

Please note that only String will get this special treatment. Custom objects don't get this - unless you use const.

However, String interned doesn't work on String construction like this:

string_construction
var c = String.fromCharCodes([72, 101, 108, 108, 111]); // 'Hello'
var d = 'Hello';
print(identical(c, d)); // false - c was constructed at runtime

Best Practices

Now that you know about those interesting facts, here are some best practices you can follow:

1. Use const everywhere you can

Without const, Flutter creates new objects every time the widget rebuilds. This can happen dozens of times per second during animations or state changes.

bad_example
//
Widget build(BuildContext context) {
  return Column(
    children: [
      SizedBox(height: 16),
      Text('Label'),
    ],
  );
}

By adding const, Flutter reuses the same objects from memory instead of creating new ones. This makes your app faster and more efficient.

good_example
Widget build(BuildContext context) {
  return Column(
    children: [
      const SizedBox(height: 16),
      const Text('Label'),
    ],
  );
}

2. Mix const and non-const according to their use

You can't use const when dealing with dynamic data. The key is to use const for static parts that never change (like spacing widgets, labels, and layout elements) while keeping dynamic parts non-const.

Widget build(BuildContext context) {
  return Column(
    children: [
      Text(dynamicData),              // Non-const - changes
      const SizedBox(height: 16),     // Const - never changes
      const Text('Static Label'),     // Const - never changes
    ],
  );
}

3. Const in Lists

Const works great in collections!

In this example, even though spacing appears 3 times in the list, there's only one SizedBox(height: 16) object in memory. This is powerful when building lists of widgets where many elements share common components.

// Even in lists, const works!
const spacing = SizedBox(height: 16);

// All three references point to the SAME object
var widgets = [
  const Text('First'),
  spacing,
  const Text('Second'),
  spacing,
  const Text('Third'),
  spacing,
];

If you're rendering a list of 100 items with the same spacing between them, using const means you're only storing that spacing widget once.

4. Deep Canonicalization

When you mark a widget as const, Dart canonicalizes the entire widget tree beneath it as a single unit.

// The ENTIRE tree is canonicalized as one unit
const complexWidget = Padding(
  padding: EdgeInsets.all(16),
  child: Column(
    children: [
      Text('Title'),
      SizedBox(height: 8),
      Text('Subtitle'),
    ],
  ),
);

// Every time you use complexWidget, it's the exact same object!

In the above example, the Padding, its EdgeInsets, the Column, both Text widgets, and the SizedBox are all treated as 1 immutable object.

This means if you use complexWidget in 50 different places across your app, you're referencing the exact same tree 50 times.


When not to Use const

You can't use const when:

  1. Values are determined at runtime:
Text(userName)  // userName comes from API/state
  1. Widgets need to rebuild:
AnimatedOpacity(
  opacity: _opacity,  // this changes over time
  child: const Text('Fade me'),  // but the child still can use const
)
  1. Using non-const constructors:
DateTime.now()  // runtime value (always changing)
Random().nextInt(10)  // runtime computation

Conclusion

The const keyword in Dart is powerful because it:

  • uses less memory by reusing the same objects
  • makes your app rebuild faster
  • doesn't create new objects when your app runs

Now, go add const to all those SizedBox widgets in your project! :p

Happy learning! 🚀