Dart Constructors 101

Suat Özkaya
5 min readApr 21, 2022

--

👉🏽 Part 1: Generative Constructors

A beginner-friendly tutorial for Dart constructors

Recently I have been working on a project where we are building a design system and I found myself creating and refactoring lots of constructors for these component widget classes. I realized that some details were not still clear to me. So I decided to do a deep dive into Dart Constructors.

Basically, constructors are just methods that create an object, a new instance of a class. Apart from syntax, each language may offer its own way, options, and easiness to developers, during this initiating process.

Let’s say we need a class named…. 🤔 … “User” 😀 ( a very creative name )

class User {  
String name;
int id;
}

First and foremost, we can classify constructors into two main categories.

  • Generative Constructors: creates a new instance of a class.
  • Factory Constructors: gives us more flexible options such as performing extra work before returning the instance. Also lets us return an existing object, a subclass, or even null.

For now, we will dismiss ‘factory constructors’ and just focus on ‘generative constructors’ in Part 1. Later, understanding factories will be… easy peasy 😉

Default Constructor

Any time we create a class, even if we do not define it implicitly, Dart gives us a default constructor behind the scenes.

class User{
String name;
int id;
}
User aNewUser = User();

However, most of the time we need to create a customized object because you know each user has his/her own name and id. So we need custom constructors at this point.

Custom Constructor

We just need a simple method in our class and supply the arguments which need to be customized:

class User {
String name;
int id;
// custom constructor with positional required parameters
User(String name, int id) {
this.name = name;
this.id = id;
}
}User user001 = User('Bob', 30);

In this case, the arguments passed to the constructor are required (not optional) and their order (position) of them is relevant. Hence we can call name and id as positional required parameters.

And of course, Dart has other options for parameters:

  • A parameter can be named as an alternative to positional
  • A parameter can be optional instead of required

But before explaining these alternative parameter types, let me remind you that there is a shorter way (syntactic sugar) for creating constructors more easily. If property (instance field) names are the same as parameter names, then why are we always typing the same things again and again and again…

Instead of getting parameters, passing them to the body and assigning them to this.xxx , why don’t we just directly perform this assignment in parentheses as below?

// Long form
User(String name, int id) {
this.id = id;
this.name = name;
}
// Short form
User(this.name, this.id)

⚠️ This short form is the recommended styling by Dart documents 👉🏽https://dart-lang.github.io/linter/lints/prefer_initializing_formals.html

So our class now looks as below. All are the same: again we have positional required parameters, but now our constructor is in a shorter, developer-friendly form. And from now on, I will be using this shorter form.

class User {
String name;
int id;
User(this.name, this.id);}User user001 = User('Bob', 30);

⚠️ You can still use curly braces for any initialization code here!

Ok, now we can get back to the parameter options that Dart supplies.

Let’s say we added a field called address that is optional. Then we just need to wrap this argument with ‘square brackets’ [……].

class User {
String name; //required
int id; //required
String? address; //optional
User(this.name, this.id, [this.address]);}User user001 = User('Bob', 30, 'Ankara');

⚠️ Since the address field is optional, we must either define it as nullable or assign a default value during construction => User(this.name, this.id, [this.address = 'Ankara'])

Most of the time, using positional arguments is not a convenient solution since there may be lots of arguments. And we have another solution: named arguments. Let’s say we want to define address as required named and phone as optional named arguments:

class User {
String name; //positional required
int id; //positional required
String address; //named required
String? phone; //named optional
User(this.name, this.id, {required this.address, this.phone});}User user001 = User('Bob', 30, address:'Ankara', phone:'555-00-00');
A brief summary of all options

Named Constructors

In Dart, we also have the option to give constructors explicit names. Very well-known examples may be either ‘fromJson’ named constructor of data classes or EdgeInsets named constructors in Flutter.

class User {
String name;
int id;
// Custom generative constructor
User(this.name, this.id);
// Named generative constructor
User.fromJson(String json)
: // some initialization code here
}

⚠️ Implementing fromJson as a factory constructor would be better in most cases, we will discuss this in part2

⚠️ Reminder: We generally also have a toJson method in a similar case, and this toJson is a method, not a constructor!

EdgeInsets class, Flutter framework

As you see in the example above, we have the flexibility to demand any parameter from the developer and initialize a new EdgetInsets object with any possibility. Named constructors obviously improve readability and gives us limitless ways to create objects with various configuration.

Initializer List

In some cases, we need to tweak the initialization of instance variables with given arguments. We can initialize desired variables after a colon (:) and separate them with commas. You can see this case in the EdgeInsets example.

There are other details such as redirecting (forwarding), constructor bodies, const keyword, etc. However, this level of detail is sufficient for this article. The next article will cover the factory constructors.

--

--