Getting Familiar with Dart

Classes and Objects


Learning Objectives

  • You know how to define classes and create objects in Dart.
  • You know of inheritance and how to extend classes in Dart.
  • You know how to define enums and use them as types.

Dart is an object-oriented programming language. Like many object-oriented programming languages, it offers ways to couple concepts together as well as to create entities that are used to divide and conquer aspects of a program.

Defining a class and creating an object

Classes in Dart are defined using the keyword class, which is followed by the name of the class and the block that contains the class definition. The class definition block can contain instance properties, instance methods, and constructors.

The following example defines a class called Person that has a name and a year of birth. The class also has a constructor that takes the name and the year of birth as parameters, and a method toString that returns a string representation of the object. The @override annotation is used to indicate that the method is overriding a method from the parent class — like in e.g. Java, all classes in Dart inherit from the class Object.

class Person {
  String name;
  int yearOfBirth;

  Person(this.name, this.yearOfBirth);

  @override
  String toString() {
    return "$name ($yearOfBirth)";
  }
}

Creating an instance of a class is done by calling a constructor. The following program shows creating an instance of the class Person, and then printing the object using the toString method.

Run the program to see the output

Unlike e.g. with Java, a new keyword is not used when calling a constructor.

Instance properties

Each object has its own values for the instance properties. Instance properties of an object are accessed using the dot notation, where the name of the object comes before the dot and the name of the instance property comes after the dot.

As an example, the following program would create two instances of the class Person and print their names.

void main() {
  final jean = Person("Jean Sibelius", 1865);
  final alvar = Person("Alvar Aalto", 1898);

  print(jean.name);
  print(alvar.name);
}

Instance methods

Instance methods are functions that have access to the instance properties of the object that the method is invoked on. In the following example, we define an instance method that is used to determine whether the person has been born after another person.

Run the program to see the output

The example also demonstrates defining the type of return values as well as the type of the parameters. If we would wish to be explicit about instance properties, we could use the keyword this, writing the function bornAfter as follows.

bool bornAfter(Person person) {
  return this.yearOfBirth > person.yearOfBirth;
}

Comparing for equality

Objects can be compared with the == operator. By default, for objects, the == operator compares the memory addresses of the objects. Even if two objects have the same values for their instance properties, they are not considered equal if they are not the same object in memory.

To compare two objects for equality, we need to override the == operator. The following example demonstrates overriding the == operator for the class Person. The == operator is overridden to compare the name and the year of birth of the objects.

Run the program to see the output

Loading Exercise...

Constructors

In the above examples, we used a constructor that sets instance property values automatically, at least from the perspective of e.g. Java programmers (prior to the introduction of record classes). There was no explicit need to define a constructor block to assign the instance property values.

When we write a constructor that follows the form Name(this.property, this.another, ...), we override a default constructor inherited from Object. Each class can have only one default constructor, and defining multiple constructors in the said format leads to an error.

Classes can have multiple constructors, but only one default constructor (that shares the name of the class). Additional constructors can be created as named constructors, where each constructor has a specific name.

Named constructors

Creating a named constructor is done by providing the class name followed by a dot and the name for the constructor, which is then followed by the parameters.

In the following example, we define a named constructor unknownBirthYear to the class Person, where the birth year would then be set to -9999 upon creation — the named constructor is followed by a colon, which is followed by initialization of properties that are not directly initialized in the constructor.

Run the program to see the output

It is also possible to call another constructor from a constructor. This is done by using the this keyword followed by the name of the constructor. The following example demonstrates calling the default constructor from the named constructor unknownBirthYear.

class Person {
  String name;
  int yearOfBirth;

  Person(this.name, this.yearOfBirth);
  Person.unknownBirthYear(this.name) : this(name, -9999);

  // ...
}

Named arguments and default values

Like when working with functions, it is also possible to explicitly define the names of the arguments when calling a constructor. This makes the use of specific arguments more explicit. In the following example, we adjust the named constructor unknownBirthYear of the class Person so that it takes the name of the person as a named argument, using “Unknown” as the default value.

Run the program to see the output

Inheritance

Dart supports single inheritance, meaning that a class can inherit from only one superclass. In Dart, the extends keyword is used to indicate that a class is a subclass of another class. The following example demonstrates defining a class Student that extends the class Person.

class Student extends Person {
  String studentNumber;

  Student(String name, int yearOfBirth, this.studentNumber) : super(name, yearOfBirth);

  @override
  String toString() {
    return "$name ($yearOfBirth) - $studentNumber";
  }
}

There exists also ready-made classes that can be extended to add functionality to programs. As an example, the package equatable provides a class Equatable that can be inherited to generate the functionality for comparing equality of objects.

The following example shows using the Equatable class. In the example, we define a class Task that extends the Equatable class. The Task class has a name and a description, and the props method is overridden to return the properties that are used to compare the objects for equality.

Run the program to see the output

Try the above example without extending the Equatable class and see what happens when comparing the objects for equality.

Sealed classes

Dart also has a concept of sealed classes. Sealed classes can be inherited only from within the same file in which the sealed class is defined — this allows restricting the subclasses of a class. The sealed keyword is used to define a sealed class

The following example demonstrates defining a sealed class Shape that has subclasses Square and Triangle.

sealed class Shape {
}

class Square extends Shape {
  final double side;

  Square(this.side);
}

class Triangle extends Shape {
  final double base;
  final double height;

  Triangle(this.base, this.height);
}

Sealed classes allow providing better support for pattern matching and exhaustiveness checking. In particular, when sealed classes are used in switch expressions, the compiler can check that all subclasses are handled.

In the following example, we take the above class Shape, and define a function area that calculates the area of a shape. The function uses a switch expression to determine the area of the shape based on the type of the shape.

However, as the switch expression is not exhaustive, i.e. all subclasses of the sealed class Shape are not handled, the compiler will give an error.

Run the program to see the output

If you add handling for the Triangle class, the error will disappear.

Loading Exercise...

Enums

Programming languages also often offer ways to define named constants. In Dart, this is done using the enum keyword, followed by the name of the enum and the list of constant values. The following example defines an enum called Day that has the constants monday, tuesday, wednesday, thursday, friday, saturday, and sunday.

enum Day {
  monday,
  tuesday,
  wednesday,
  thursday,
  friday,
  saturday,
  sunday
}

By convention, enum values are written in Dart using lowerCamelCase. Enums can be used as types, and the constants can be accessed using the dot notation. The following example demonstrates creating a variable of type Day and assigning it the value Day.monday.

void main() {
  final day = Day.monday;
  print(day);
}

Enums can also have properties and methods. The following example demonstrates defining an enum called Shape that has the constants circle, square, and triangle, and a “getter” property sides that returns the number of sides of the shape. The getter uses a switch expression for determining the number of sides.

Run the program to see the output

The following example demonstrates defining an enum called Direction that has the constants north, east, south, and west, and a method turnRight that returns the direction to the right of the current direction.

Run the program to see the output

Loading Exercise...