Deconstructing Dart Constructors
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.
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.