Flutter and Dart: When to use var, final and const
Learn when to use var, final, or const in your Flutter and Dart apps. As well as some performance benefits of const and the static modifier.
This article demonstrates samples of when to use which keyword and some performance considerations for your Flutter or Dart app. We'll also discus the static
modifier.
As a quick summar, in Dart the order of preference you should prefer is:
const
final
var
Let's explore more 😎
Mutable vs Immutable
It's important to understand the difference between mutable and immutable before we continue.
A naive approach would be to always want an object to change - for it to be mutable. However, you will often find that in larger applications the code is a lot easier to reason about with immutable objects.
However, this is a topic that will have to wait for a future article. If you're interested you can read this Stack Overflow question for more opinions.
Exploring var
var
allows you to assign a variable without specifying a type. The type will be inferred from the initial value:
var number = 1;
The variable number
will be an int
, and you can update the value:
number += 1; // adding one to number
You cannot assign a new type to number
. The following is invalid:
number = 'test';
Collections with var
Creating a collection with var
doesn't hold any surprises. The following are all valid:
var numbers = [1, 2, 3];
numbers[0] = 0;
numbers.add(4); // you can update the list
numbers = [1, 2, 3, 4, 5]; // you can reasign the variable
When to use var
?
Use var
when you need a mutable variable that needs to be updated.
If the initial value is not known, however the type is, then you can specify the type explicitly:
late int number; // using int instead of var
// ... later in your program
number = 10; // setting a new value
A great place for var
is inside a StatefulWidget
where you often want to keep mutable state that can be updated based on some actions.
Exploring final
final
is the same as var
, the only difference is that it's immutable and cannot be updated. It can only be set once:
final number = 1;
number += 1; // This is invalid and Dart will not compile
final
provides safety to your code as it ensures that a value cannot be updated unintentionally by some other part of your program.
Collections with final
If you have a final
collection, everything inside of that is not final
🤔.
final numbers = [1, 2, 3];
numbers[0] = 0;
numbers.add(4);
All of the above is valid. It's possible to update the values of the list, however, it's not possible to re-assign the variable numbers
.
Note that two final
lists with identical values aren't equal:
if (identical([], []) == false) {
print('not identical');
}
When to use final
?
Use final
if you don't know the value at compile time, and it will be calculated or fetched at runtime. For example, if you want an HTTP response that can't be changed, or reading something from a database or local file. Anything that is not known at compile time, that you want to be immutable, should be final
.
final
and only change it to var
in the event you need to update it.Exploring const
A const
variable is a compile-time constant. The object's entire deep state can be determined at compile time and will be frozen and completely immutable.
The following are valid const
values:
const value = "A fixed string that won't change";
const calculation = 1 + 2 + 3;
const widget = Text('This is a constant widget');
const padding = EdgeInsets.all(8);
They are also canonicalized, which means a single const object will be created and re-used no matter how many times the const expression(s) are evaluated. This means that no matter how many places we use EdgeInsets.all(8)
only a single instance will ever be created.
Using a compile time constant when possible will take up less memory in our application, as one instance can be reused multiple times. We'll explore this more later.
Collections with const
If you have a const
collection, everything inside of that needs to be const
.
For example, the following is invalid:
const numbers = [1, 2, 3];
numbers[0] = 3;
print(numbers); // Exception: Cannot modify an unmodifiable list
You also cannot do:
final numberOne = 1; // Not constant
const numbers = [numberOne, 2, 3];
As numberOne
is not constant.
Note that two const
lists with identical values are equal:
if (identical(const[], const[]) == true) {
print('identical');
}
When to use const
?
- When the value can be calculated at compile time.
- If you want an unmodifiable list.
- If you want to reduce widget build cost. We'll explore this more later.
const
whenever possible. A good rule is to follow the linting suggestions specified in the flutter_lints
package - which is by default enabled on any new Flutter project.Widget Build Cost - Preferring const
Using a const
widget is one of the easiest performance wins we can achieve when developing a Flutter application.
It helps the Flutter engine to avoid unnecessary widget rebuilds, which also helps the engine to determine when the underlying RenderObjects should be reused instead of recreated.
Let's take a look at an example:
class BuildTestWidget extends StatefulWidget {
const BuildTestWidget({Key? key}) : super(key: key);
@override
State<BuildTestWidget> createState() => _BuildTestWidgetState();
}
class _BuildTestWidgetState extends State<BuildTestWidget> {
@override
Widget build(BuildContext context) {
return Column(
children: [
const Text('Constant text'),
Container(
color: Colors.red,
),
GestureDetector(
onTap: () => setState(() {}),
child: const Text('call setState'),
),
],
);
}
}
In this code we have two constant Text widgets, a non-constant Container, and a GestureDetector that calls setState
on tap. These are wrapped in a Column that cannot be constant, as not all the children are.
Exploring the build phase for this widget in Flutter DevTools, we can see the following on rebuild:
What is interesting to note is that we do not see the build for the constant Text widgets. That is because constant widgets will only be built once, and then reused as long as they are used between consecutive animation frames.
This example may seem trivial, but small things add up. We can also create more complex constant widgets than a Text widget.
The Flutter engine will now also know that the underlying RenderObjects that represent the widgets cannot have changed if the widget is constant. Meaning Flutter does not need to do any calculations to determine if the RenderObject can be reused, or whether it requires updating, it knows it has to be the same.
The same is not true for the Container widget. Flutter will first need to verify that the widget description did not change on rebuild. For example, if the color changed from red to blue, then the container's RenderObject will need updating. Luckily this whole process is really efficient and we do not need to worry about it, however whenever possible we can help the engine along.
Animating Constant Widgets
Reducing build cost when animating is especially important. You don't want to rebuild a widget 60-120 times per second if not absolutely necessary 😱. If a widget cannot be const, then you should look into caching the widget instead, see AnimatedBuilder as an example.
Memory Allocation
Let's take a look at memory allocation and see another benefit of constants.
In this code below, we're creating two widgets that both have a constant constructor. However, the TrackNotConstWidget takes in a child widget, which may not be constant.
class TrackConstWidget extends StatelessWidget {
const TrackConstWidget({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Container();
}
}
class TrackNotConstWidget extends StatelessWidget {
const TrackNotConstWidget({Key? key, required this.child}) : super(key: key);
final Widget child;
@override
Widget build(BuildContext context) {
return child;
}
}
Below, we're creating 100 of each widget, the first can be constant and the second cannot (because the child Container does not have a constant constructor).
Widget build(BuildContext context) {
return ListView(
children: [
for (var i = 0; i < 100; i++) const TrackConstWidget(),
for (var i = 0; i < 100; i++) TrackNotConstWidget(child: Container())
],
);
}
The memory allocation for this will look like:
As you can see a single object is created and reused (canonicalized) for the constant widget and 100 for the non-constant one. Using 16 bytes vs 3200 respectively.
What is static
?
static
means a member is available on the class itself instead of on instances of the class:
class Person {
final String name;
final int age;
static final species = Species('human');
Person(this.name, this.age);
}
The above allows you to call Person.species
directly on the class, without needing to create an instance. More importantly, a single instance of Species will be created and reused, regardless of how many Person instances you make.
A Practical Example Using static
Let's modify the widget we used earlier.
class TrackNotConstWidget extends StatefulWidget {
const TrackNotConstWidget({Key? key, required this.child}) : super(key: key);
final Widget child;
@override
State<TrackNotConstWidget> createState() => _TrackNotConstWidgetState();
}
class _TrackNotConstWidgetState extends State<TrackNotConstWidget> {
final Tween<double> someTween = Tween(begin: 0, end: 1);
@override
Widget build(BuildContext context) {
return widget.child;
}
}
TrackNotConstWidget is now a StatefulWidget and has a tween, called someTween
. Building this 100 times will also create 100 Tween instances.
The class Tween cannot be made constant, however, in this example we want the same tween to be used for each instance of TrackNotConstWidget. We could either cache the tween in some way, or simply make someTween
static, and that will ensure the same instance is reused 🥳.
static final Tween<double> someTween = Tween(begin: 0, end: 1);
You can read more about static tweens here and when to use them.
VSCode Tip - Auto Add Const
It can get quite annoying constantly typing const 😉. I made a video showing how to enable auto fix when using Visual Studio Code.
Save some time and let the editor do the hard work.
In VSCode:
- Press cmd-shift-p (or ctrl-shift-p)
- Search for settings
- Open Settings (JSON)
- Add the following rule
"editor.codeActionsOnSave": {
"source.fixAll": true
},
This will run dart --fix
and apply linting fixes on each save (most notably it'll auto add const where needed).
Conclusion
In this article we explored the differences between var
, final
and const
and when to use which. We also explored the benefits of const
from a widget rebuild and memory perspective. Finally we also touched on the static
keyword and how that can be used to avoid unnecessary instance creation.
If you enjoyed this article don't miss out and subscribe to get future content.