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.

Flutter and Dart: var, const or final
ON THIS PAGE

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:

  1. const
  2. final
  3. var

Let's explore more 😎

Mutable vs Immutable

💡
A mutable object can change after it's created, while an immutable object can't.

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.

Only use var if you want the value to be mutable, otherwise prefer final or const when possible.

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.

💡
As a general best practice, always make something 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.
💡
Prefer 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:

Example of constant widget rebuilds in Flutter DevTools

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:

Memory allocation for constant vs non-constant objects in Dart

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:

  1. Press cmd-shift-p (or ctrl-shift-p)
  2. Search for settings
  3. Open Settings (JSON)
  4. 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.