9 Constructors¶
People build houses; factory robots build cars; and 3D printers build models. In the programming world, constructors are methods that create, or construct, instances of a class. That is to say, constructors build new objects. Constructors have the same name as the class, and the implicit return type of the constructor method is also the same type as the class itself.
This chapter will teach you about the differences between the various types of constructor methods Dart provides for building classes, which include generative, named, forwarding and factory constructors.
Default Constructor¶
When you don’t specify a constructor, Dart provides a default constructor that takes no parameters and just returns an instance of the class. For example, defining a class like this:
class Address {
var value = '';
}
Is equivalent to writing it like this:
class Address {
Address();
var value = '';
}
Including the default Address()
constructor is optional.
Sometimes you don’t want the default constructor, though. You’d like to initialize the data in an object at the same time that you create the object. The next section shows how to do just that.
Custom Constructors¶
If you want to pass parameters to the constructor to modify how your class builds an object, you can. It’s similar to how you wrote functions with parameters in Chapter 7, “Functions”.
Like the default constructor above, the constructor name should be the same as the class name. This type of constructor is called a generative constructor because it directly generates an object of the same type.
You’ll continue to build on the User
class you wrote in Chapter 8, “Classes”. Here’s the code you had at the end of that chapter:
class User {
int id = 0;
String name = '';
String toJson() {
return '{"id":$id,"name":"$name"}';
}
@override
String toString() {
return 'User(id: $id, name: $name)';
}
}
Copy that to your project below the main
method before continuing on with the rest of the chapter.
Long-Form Constructor¶
In Dart, the convention is to put the constructor before the property variables. Add the following generative constructor method at the top of the class body:
User(int id, String name) {
this.id = id;
this.name = name;
}
This is known as a long-form constructor. You’ll understand why it’s considered “long-form” when you see the short-form constructor later.
Without the toJson
and toString
methods, this is what the class looks like:
class User {
User(int id, String name) {
this.id = id;
this.name = name;
}
int id = 0;
String name = '';
// ...
}
this
is a new keyword. What does it do?
The keyword this
in the constructor body allows you to disambiguate which variable you’re talking about. It means this object. So this.name
refers to the object property called name
, while name
(without this
) refers to the constructor parameter. Using the same name for the constructor parameters as the class properties is called shadowing. So the constructor above takes the id
and name
parameters and uses this
to initialize the properties of the object.
Write the following code in main
to create a User
object by passing in some arguments:
final user = User(42, 'Ray');
print(user);
Once you’ve created the object, you can access its properties and other methods just as you did in the last chapter. However, you can’t use the default constructor User()
anymore since id
and name
are required positional parameters.
Short-Form Constructor¶
Dart also has a short-form constructor where you don’t provide a function body, but you instead list the properties you want to initialize, prefixed with the this
keyword. Arguments you send to the short form constructor are used to initialize the corresponding object properties.
Here’s the long-form constructor you currently have:
User(int id, String name) {
this.id = id;
this.name = name;
}
Now replace that with the following short-form constructor:
User(this.id, this.name);
Dart infers the constructor parameter types of int
and String
from the properties themselves that are declared in the class body.
The class should now look like this:
class User {
User(this.id, this.name);
int id = 0;
String name = '';
// ...
}
Run the code again. You’ll see the short-form constructor works just like the longer form you replaced, but it’s just a little tidier now.
Note
You could remove the default property values of 0
and ''
at this point since id
and name
are guaranteed to be initialized by the constructor parameters. However, there’s an intermediate step in the next section where they’ll still be useful. Keeping the default values a little longer will allow this chapter to postpone dealing with null safety until it can be covered more fully in Chapter 11, “Nullability”.
Named Constructors¶
Dart also has a second type of generative constructor called a named constructor, which you create by adding an identifier to the class name. It takes the following pattern:
ClassName.identifierName()
From here on, this chapter will refer to a constructor without the identifier as an unnamed constructor:
// unnamed constructor
ClassName()
// named constructor
ClassName.identifierName()
Why would you want a named constructor instead of the nice, tidy default one? Well, sometimes you have some common cases that you want to provide a convenience constructor for. Or maybe you have some special edge cases for constructing certain classes that need a slightly different approach.
Say, for example, that you want to have an anonymous user with a preset ID and name. You can do that by creating a named constructor. Add the following named constructor below the short-form constructor:
User.anonymous() {
id = 0;
name = 'anonymous';
}
The identifier, or named part, of the constructor is .anonymous
. Named constructors may have parameters, but in this case, there are none. And since there aren’t any parameter names to get confused with, you don’t need to use this.id
or this.name
. Rather, you just use the property variables id
and name
directly.
Call the named constructor in main
like so:
final anonymousUser = User.anonymous();
print(anonymousUser);
Run that and you’ll see the expected output:
User(id: 0, name: anonymous)
Note
Without default values for id
and name
, Dart would have complained that these variables weren’t being initialized, even though User.anonymous
does in fact initialize them in the constructor body. You could solve the problem by using the late
keyword, but that’s a topic for Chapter 11, “Nullability” — hence the default values here.
Forwarding Constructors¶
In the named constructor example above, you set the class properties directly in the constructor body. However, this doesn’t follow the DRY principle you learned earlier. You’re repeating yourself by having two different locations where you can set the properties. It’s not a huge deal, but imagine that you have five different constructors instead of two. It would be easy to forget to update all five if you had to make a change. And if the constructor logic were complicated, it would be easy to make a mistake.
One way to solve this issue is by calling the main constructor from the named constructor. This is called forwarding or redirecting. To do that, you use the keyword this
again.
Delete the anonymous
named constructor that you created above and replace it with the following:
User.anonymous() : this(0, 'anonymous');
This time there’s no constructor body, but instead, you follow the name with a colon and then forward the properties to the unnamed constructor. The forwarding syntax replaces User
with this
.
Also, now that you’ve moved property initialization from the constructor body to the parameter list, Dart is finally convinced that id
and name
are guaranteed to be initialized.
Replace these two lines:
int id = 0;
String name = '';
with the following:
int id;
String name;
No complaints from Dart.
You call the named constructor exactly like you did before:
final anonymousUser = User.anonymous();
The results are the same as well.
Optional and Named Parameters¶
Everything you learned about function parameters in Chapter 7, “Functions”, also applies to constructor method parameters. That means you can make parameters optional using square brackets:
MyClass([this.myProperty]);
Or you can make them optional and named using curly braces:
MyClass({this.myProperty});
Or named and required using curly braces and the required
keyword:
MyClass({required this.myProperty});
Adding Named Parameters for User¶
Earlier when you instantiated a User
object, you wrote this:
final user = User(42, 'Ray');
For someone not familiar with your User
class, they might think 42
is Ray’s age, or his password, or how many pet cats he has. Using named parameters here would help a lot with readability.
Refactor the unnamed constructor in User
by adding braces around the parameters. Since that makes the parameters optional, you could use the required
keyword, but this time simply give them default values:
User({this.id = 0, this.name = 'anonymous'});
The compiler will complain about this because that change also requires refactoring the anonymous
named constructor to use the parameter names in the redirect.
However, since the parameter defaults are now just what the anonymous
constructor was doing before, you can delete the anonymous
constructor and replace it with the following:
User.anonymous() : this();
Using the named constructor will now forward to the unnamed constructor with no arguments.
In main
, the way to create an anonymous user is still the same, but now the way to create Ray’s user object is like so:
final user = User(id: 42, name: 'Ray');
That’s a lot more readable. They’re not cats; it’s clearly just an ID.
In case you’ve gotten lost, here’s what things look like now:
void main() {
final user = User(id: 42, name: 'Ray');
print(user);
final anonymousUser = User.anonymous();
print(anonymousUser);
}
class User {
// unnamed constructor
User({this.id = 0, this.name = 'anonymous'});
// named constructor
User.anonymous() : this();
int id;
String name;
// ...
}
Initializer Lists¶
You might have discovered a small problem that exists with your class as it’s now written. Take a look at the following way that an unscrupulous person could use this class:
final vicki = User(id: 24, name: 'Vicki');
vicki.name = 'Nefarious Hacker';
print(vicki);
// User(id: 24, name: Nefarious Hacker)
If those statements were spread throughout the codebase instead of being in one place as they are here, someone printing the vicki
user object and expecting a real name would get a surprise. “Nefarious Hacker” is definitely not what you’d expect. Once you’ve created the User
object, you don’t want anyone to mess with it.
But forget nefarious hackers; you’re the one who’s most likely to change a property and then forget you did it.
There are a couple of ways to solve this problem. You’ll see one solution now and a fuller solution later.
Private Variables¶
As you learned in the previous chapter, Dart allows you to make variables private by adding an underscore _
in front of their name.
Change the id
property to _id
and name
to _name
. Since these variables are used in several locations throughout your code, let VS Code help you out. Put your cursor on the variable name and press F2
. Edit the variable name and press Enter to change all of the references at once.
Oh…, that actually renamed a few more things than you intended since it also renamed what was in the main
function. But that’s OK. Just delete everything inside the body of main
for now.
There is still one problem left with the unnamed constructor in User
:
User({this._id = 0, this._name = 'anonymous'});
The compiler gives you an error:
Named parameters can’t start with an underscore.
Fix that by deleting the unnamed constructor and replacing it with the following:
User({int id = 0, String name = 'anonymous'})
: _id = id,
_name = name;
Do you see the colon that precedes _id
? The comma-separated list that comes after it is called the initializer list. One use for this list is exactly what you’ve done here. Externally, the parameters have one name, while internally, you’re using private variable names.
The initializer list is always executed before the body of the constructor if the body exists. You don’t need a body for this constructor, but if you wanted to add one, it would look like this:
User({int id = 0, String name = 'anonymous'})
: _id = id,
_name = name {
print('User name is $_name');
}
The constructor would initialize _id
and _name
before it ran the print
statement inside the braces.
Why Aren’t the Private Properties Private?¶
It turns out that your nefarious hacker can still access the “private” fields of User
. Add the following two lines to main
to see this in action:
final vicki = User(id: 24, name: 'Vicki');
vicki._name = 'Nefarious Hacker';
What’s that all about? Well, using an underscore before a variable or method name makes it library private, not class private. For your purposes in this book, a library is simply a file. Since the main
function and the User
class are in the same file, nothing in User
is hidden from main
. To see private variables in action, you’ll need to make another file so that you aren’t using your class in the same file in which it’s defined.
Create a new folder in the root of your project called lib, which is short for “library”. This is the standard name and location where Dart expects to find the project files you want to import. To create the new folder in VS Code, you can click on the project folder in the Explorer panel and then press the New Folder button:
Next, create a new file in lib called user.dart. You can click on lib in the Explorer panel and then press the New File button:
Now move the entire User
class over to lib/user.dart.
If you’re using the starter project, your folder structure should look like this now:
Go back to bin/starter.dart, the one with the main
function, and add the library import to the top of the class:
import 'package:starter/user.dart';
Here are some notes:
- If you aren’t using the starter project, replace
starter
with whatever your project name is. - There’s no reference to the
lib
folder here. Dart knows that thepackage
files with your project name are in thelib
folder. A package is a collection of library files.
Now you’ll notice that in the main
function you no longer have access to _name
:
vicki._name = 'Nefarious Hacker';
This produces an error:
The setter '_name' isn't defined for the type 'User'.
Great! Now it’s no longer possible to change the properties after the object has been created. Delete or comment out that entire line:
// vicki._name = 'Nefarious Hacker';
Constant Constructors¶
You’ve already learned how to keep people from modifying the properties of a class by making them private. Another thing you can do is to make the properties immutable, that is, unchangeable. By using immutable properties, you don’t even have to make them private.
Making Properties Immutable¶
There are two ways to mark a variable immutable in Dart: final
and const
. However, since the compiler won’t know what the properties are until runtime, your only choice here is to use final
.
In the User
class in lib/user.dart, add the final
keyword before both property declarations. Your code should look like this:
final String _name;
final int _id;
Adding final
means that _name
and _id
can only be given a value once, that is, when the constructor is called. After the object has been created, those properties will be immutable. You should keep the String
and int
type annotations because removing them would cause the compiler to fall back to dynamic
.
Making Classes Immutable¶
If the objects of a particular class can never change because all fields of the class are final
, you can add const
to the constructor to ensure that all instances of the class will be constants at compile time.
Since both the fields of your User
class are now final
, this class is a good candidate for a compile-time constant.
Replace both constructors with the following:
const User({int id = 0, String name = 'anonymous'})
: _id = id,
_name = name;
const User.anonymous() : this();
Note the const
keyword in front of both constructors.
Now you can declare your User
objects as compile-time constants like so:
const user = User(id: 42, name: 'Ray');
const anonymousUser = User.anonymous();
Benefits of Using Const¶
In addition to being immutable, another benefit of const
variables is that they’re canonical instances, which means no matter how many instances you create, as long as the properties used to create them are the same, Dart will only see a single instance. You could instantiate User.anonymous()
a thousand times across your app without incurring the performance hit of having a thousand different objects.
Note
Flutter uses this pattern frequently with its const
widget classes in the user interface of your app. Since Flutter knows that the const
widgets are immutable, it doesn’t have to waste time recalculating and drawing the layout when it finds these widgets.
Make it your goal to use const
objects and constructors as much as possible. It’s a performance win!
Exercise¶
Given the following class:
class PhoneNumber {
String value = '';
}
- Make
value
afinal
variable, but not private. - Add a
const
constructor as the only way to initialize aPhoneNumber
object.
Factory Constructors¶
All of the constructors that you’ve seen up until now have been generative constructors. Dart also provides another type of constructor called a factory constructor.
A factory constructor provides more flexibility in how you create your objects. A generative constructor can only create a new instance of the class itself. However, factory constructors can return existing instances of the class, or even subclasses of it. You’ll learn about subclasses in Dart Apprentice: Beyond the Basics, Chapter 3, “Inheritance”. This is useful when you want to hide the implementation details of a class from the code that uses it.
The factory constructor is basically a special method that starts with the factory
keyword and returns an object of the class type. For example, you could add the following factory constructor to your User
class:
factory User.ray() {
return User(id: 42, name: 'Ray');
}
The factory method uses the generative constructor to create and return a new instance of User
. You could also accomplish the same thing with a named constructor, though.
A more common example you’ll see is using a factory constructor to make a fromJson
method:
factory User.fromJson(Map<String, Object> json) {
final userId = json['id'] as int;
final userName = json['name'] as String;
return User(id: userId, name: userName);
}
You would create a User
object from the constructor like so:
final map = {'id': 10, 'name': 'Sandra'};
final sandra = User.fromJson(map);
You’ll learn how the Map
collection works in Chapter 14, “Maps”. The thing to pay attention to now is that the factory constructor body allows you to perform some work before returning the new object, and you didn’t exposing the details of that work to whoever is using the class.
For example, you could create a User.fromJson
constructor with a named constructor like so:
User.fromJson(Map<String, Object> json)
: _id = json['id'] as int,
_name = json['name'] as String;
However, there isn’t much else you can do with id
and name
. With a factory constructor, though, you could do all kinds of validation, error checking and even modification of the arguments before creating the object. This is actually highly desirable in the case here because if 'id'
or 'name'
didn’t exist in the map, then your app would crash because you aren’t handling null
.
Note
Using a factory constructor over a named constructor can also help to prevent breaking changes for subclasses of your class. That topic is a little beyond the scope of this chapter, but you can read https://stackoverflow.com/a/66117859 for a longer explanation.
You’ll see a few more uses of the factory constructor in Chapter 10, “Static Members”.
Constructor Comparisons¶
Since there are so many ways that constructors can vary, here’s a brief comparison.
Constructors can be:
- Forwarding or non-forwarding.
- Named or unnamed.
- Generative or factory.
- Constant or not constant.
- With parameters or without.
- Short-form or long-form.
- Public or private.
Many of those options can vary independently of each other. For example, the following is a public, non-forwarding, unnamed, generative, const constructor with parameters.
const User(this.id, this.name);
Or here’s a private, non-forwarding, named, generative, non-constant constructor without parameters:
DatabaseHelper._internal();
You’ll learn one use for private constructors in the next chapter when you get to the section on singletons. See you there!
Challenges¶
Before moving on, here is a challenge to test your knowledge of constructors. It’s best if you try to solve it yourself, but a solution is available if you get stuck. It’s located with the supplementary materials for this book.
Challenge 1: Bert and Ernie¶
Create a Student
class with final firstName
and lastName
string properties and a variable grade
as an int
property. Add a constructor to the class that initializes all the properties. Add a method to the class that nicely formats a Student
for printing. Use the class to create students bert
and ernie
with grades of 95 and 85, respectively.
Key Points¶
- You create an object from a class by calling a constructor method.
- Generative constructors can be unnamed or named.
- Unnamed generative constructors have the same name as the class, while named generative constructors have an additional identifier after the class name.
- You can forward from one constructor to another by using the keyword
this
. - Initializer lists allow you to initialize field variables.
- Adding
const
to a constructor allows you to create immutable, canonical instances of the class. - Factory constructors allow you to hide the implementation details of how you provide the class instance.