- Published on
Do you know these facts about const in Flutter?
- Authors
- Name
- Rosa Tiara
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?
point1andpoint2are the same object in memory because they are declared usingconst.point3andpoint4are two separate objects, even though they have identical values, because they are declared usingfinal.- 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.
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.
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:
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.
//
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.
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:
- Values are determined at runtime:
Text(userName) // userName comes from API/state
- Widgets need to rebuild:
AnimatedOpacity(
opacity: _opacity, // this changes over time
child: const Text('Fade me'), // but the child still can use const
)
- 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! 🚀