Deconstructing Dart Constructors

Flutter Igniter
Flutter Community
Published in
5 min readSep 22, 2019

Ever confused by that mysterious syntax in Dart constructors? Colons, named parameters, asserts, factories…

Read this post and you will become an expert!

When we want an instance of a certain class we call a constructor, right?

In Dart 2 we can leave out the new:

A constructor is used to ensure instances are created in a coherent state. This is the definition in a class:

This constructor has no arguments so we can leave it out and write:

The default constructor is implicitly defined.

Photo by Arseny Togulev on Unsplash

Initializing…

Most times we need to configure our instances. For example, pass in the height of a robot:

r is now a 5-feet tall Robot.

To write that constructor we include the height field after the colon :

or even

This is called an initializer. It accepts a comma-separated list of expressions that initialize fields with arguments.

Fortunately, Dart gives us a shortcut. If the field name and type are the same as the argument in the constructor, we can do:

Imagine that the height field is expressed in feet and we want clients to supply the height in meters. Dart also allows us to initialize fields with computations from static methods (as they don't depend on an instance of the class):

Sometimes we must call super constructors when initializing:

Notice that super(...) must always be the last call in the initializer.

And if we needed to add more complex guards (than types) against a malformed robot, we can use assert:

Accessors and mutators

Back to our earlier robot definition:

Let’s make it taller:

But robots don’t grow, their height is constant! Let’s prevent anyone from modifying the height by making the field private.

In Dart, there is no private keyword. Instead, we use a convention: field names starting with _ are private (library-private, actually).

Great! But now there is no way to access r.height. We can make the height property read-only by adding a getter:

Getters are functions that take no arguments and conform to the uniform access principle.

We can simplify our getter by using two shortcuts: single expression syntax (fat arrow) and implicit this:

Actually, we can think of public fields as private fields with getters and setters. That is:

is equivalent to:

Keep in mind initializers only assign values to fields and it is therefore not possible to use a setter in an initializer:

Constructor bodies

If a setter needs to be called, we’ll have to do that in a constructor body:

We can do all sorts of things in constructor bodies, but we can’t return a value!

Final fields

Final fields are fields that can only be assigned once.

Inside our class, we won’t be able to use the setter:

The following won’t work because height, being final, must be initialized. And initialization happens before the constructor body is run:

Let’s fix it:

Default values

If most robots are 5-feet tall then we can avoid specifying the height each time. We can make an argument optional and provide a default value:

So we can just call:

Immutable robots

Our robots clearly have more attributes than a height. Let’s add some more!

As all fields are final, our robots are immutable! Once they are initialized, their attributes can't be changed.

Now let’s imagine that robots respond to many different names:

Dang, using a List made our robot mutable again!

We can solve this with a const constructor:

const can only be used with expressions that can be computed at compile time. Take the following example:

const instances are canonicalized which means that equal instances point to the same object in memory space when running.

For example this is a “cheap” operation:

And yes, using const constructors can improve performance in Flutter applications.

Optional arguments always last!

If we wanted the weight argument to be optional we'd have to declare it at the end:

Naming things

Having to construct a robot like Robot(5, ["Walter"]) is not very explicit.

Dart has named arguments! Naturally, they can be provided in any order and are all optional by default:

But we can annotate a field with @required:

(or use assert(weight != null) in the initializer!)

How about making the attributes private?

It fails! Unlike with positional arguments, we need to specify the mappings in the initializer:

Need default values?

We simply employ the handy “if-null” operator ??.

Or, for example, a static function that returns default values:

Using public fields has a nicer API:

Mixing it up

Both positional and named argument styles can be used together:

Named constructors

Not only can arguments be named. We can give names to any number of constructors:

What happened in copy? We used this to call the default constructor, effectively "redirecting" the instantiation.

( new is optional but I sometimes like to use it, since it clearly states the intent.)

Invoking named super constructors works as expected:

Note that named constructors require an unnamed constructor to be defined!

Keeping it private

But what if we didn’t want to expose a public constructor? Only named?

We can make a constructor private by prefixing it with an underscore:

Applying this knowledge to our previous example:

The named constructor is “redirecting” to the private default constructor (which in turn delegates part of the creation to its Machine ancestor).

Consumers of this API only see Robot.named() as a way to get robot instances.

A robot factory

We said constructors were not allowed to return. Guess what?

Factory constructors can!

Factory constructors are syntactic sugar for the “factory pattern”, usually implemented with static functions.

They appear like a constructor from the outside (useful for example to avoid breaking API contracts), but internally they can delegate instance creation invoking a “normal” constructor. This explains why factory constructors do not have initializers.

Since factory constructors can return other instances (so long as they satisfy the interface of the current class), we can do very useful things like:

  • caching: conditionally returning existing objects (they might be expensive to create)
  • subclasses: returning other instances such as subclasses

They work with both normal and named constructors!

Here’s our robot warehouse, that only supplies one robot per height:

Finally, to demonstrate how a factory would instantiate subclasses, let’s create different robot brands that calculate prices as a function of height:

Singletons

Singletons are classes that only ever create one instance. We think of this as a specific case of caching!

Let’s implement the singleton pattern in Dart:

The factory constructor Robot(height) simply always returns the one and only instance that was created when loading the Robot class. (So in this case, I prefer not to use new before Robot.)

That sums up pretty much the whole universe of Dart constructors! Is there anything you didn’t understand? Let us know!

Originally posted on Flutter Igniter.

--

--