Chapters

Hide chapters

Dart Apprentice: Fundamentals

First Edition · Flutter · Dart 2.18 · VS Code 1.71

Dart Apprentice: Fundamentals

Section 1: 16 chapters
Show chapters Hide chapters

9. Constructors
Written by Jonathan Sande

Heads up... You're reading this book for free, with parts of this chapter shown beyond this point as scrambled text.

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”.

class User {
  int id = 0;
  String name = '';

  String toJson() {
    return '{"id":$id,"name":"$name"}';
  }

  @override
  String toString() {
    return 'User(id: $id, name: $name)';
  }
}

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;
}
class User {
  User(int id, String name) {
    this.id = id;
    this.name = name;
  }

  int id = 0;
  String name = '';

  // ...
}
final user = User(42, 'Ray');
print(user);

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.

User(int id, String name) {
  this.id = id;
  this.name = name;
}
User(this.id, this.name);
class User {
  User(this.id, this.name);

  int id = 0;
  String name = '';

  // ...
}

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()
// unnamed constructor
ClassName()

// named constructor
ClassName.identifierName()
User.anonymous() {
  id = 0;
  name = 'anonymous';
}
final anonymousUser = User.anonymous();
print(anonymousUser);
User(id: 0, name: anonymous)

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.

User.anonymous() : this(0, 'anonymous');
int id = 0;
String name = '';
int id;
String name;
final anonymousUser = User.anonymous();

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]);
MyClass({this.myProperty});
MyClass({required this.myProperty});

Adding Named Parameters for User

Earlier when you instantiated a User object, you wrote this:

final user = User(42, 'Ray');
User({this.id = 0, this.name = 'anonymous'});
User.anonymous() : this();
final user = User(id: 42, name: 'Ray');
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)

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.

User({this._id = 0, this._name = 'anonymous'});
Named parameters can’t start with an underscore.
User({int id = 0, String name = 'anonymous'})
      : _id = id,
        _name = name;
User({int id = 0, String name = 'anonymous'})
    : _id = id,
      _name = name {
  print('User name is $_name');
}

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';

import 'package:starter/user.dart';
vicki._name = 'Nefarious Hacker';
The setter '_name' isn't defined for the type 'User'.
// 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.

final String _name;
final int _id;

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.

const User({int id = 0, String name = 'anonymous'})
    : _id = id,
      _name = name;

const User.anonymous() : this();
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.

Exercise

Given the following class:

class PhoneNumber {
  String value = '';
}

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.

factory User.ray() {
  return User(id: 42, name: 'Ray');
}
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);
}
final map = {'id': 10, 'name': 'Sandra'};
final sandra = User.fromJson(map);
User.fromJson(Map<String, Object> json)
    : _id = json['id'] as int,
      _name = json['name'] as String;

Constructor Comparisons

Since there are so many ways that constructors can vary, here’s a brief comparison.

const User(this.id, this.name);
DatabaseHelper._internal();

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.
Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.
© 2024 Kodeco Inc.

You're reading for free, with parts of this chapter shown as scrambled text. Unlock this book, and our entire catalogue of books and videos, with a Kodeco Personal Plan.

Unlock now