[Dart Language Tour] Classes, Enums, Callable Classes, Metadata

Classes, Enums, Callable Classes, Metadata

Classes

Dart is an object-oriented language with classes and mixin-based inheritance. Every object is an instance of a class, and all classes descend from Object. Mixin-based inheritance means that although every class (except for Object) has exactly one superclass, a class body can be reused in multiple class hierarchies. Extension methods are a way to add functionality to a class without changing the class or creating a subclass.

Using class members methods and instance variables

Objects have members consisting of functions and data (methods and instance variables, respectively). When you call a method, you invoke it on an object: the method has access to that object’s functions and data.

Use a dot (.) to refer to an instance variable or method:

1
2
3
4
5
6
7
var p = Point(2, 2);

// Get the value of y.
assert(p.y == 2);

// Invoke distanceTo() on p.
double distance = p.distanceTo(Point(4, 4));

Use ?. instead of . to avoid an exception when the leftmost operand is null:

1
2
// If p is non-null, set a variable equal to its y value.
var a = p?.y;

Using constructors

You can create an object using a constructor. Constructor names can be either ClassName or ClassName.identifier. For example, the following code creates Point objects using the Point() and Point.fromJson() constructors:

1
2
var p1 = Point(2, 2);
var p2 = Point.fromJson({'x': 1, 'y': 2});

The following code has the same effect, but uses the optional new keyword before the constructor name:

1
2
var p1 = new Point(2, 2);
var p2 = new Point.fromJson({'x': 1, 'y': 2});

Version note: The new keyword became optional in Dart 2.


Some classes provide constant constructors. To create a compile-time constant using a constant constructor, put the const keyword before the constructor name:

1
var p = const ImmutablePoint(2, 2);

Constructing two identical compile-time constants results in a single, canonical instance:

1
2
3
4
var a = const ImmutablePoint(1, 1);
var b = const ImmutablePoint(1, 1);

assert(identical(a, b)); // They are the same instance!

Within a constant context, you can omit the const before a constructor or literal. For example, look at this code, which creates a const map:

1
2
3
4
5
// Lots of const keywords here.
const pointAndLine = const {
'point': const [const ImmutablePoint(0, 0)],
'line': const [const ImmutablePoint(1, 10), const ImmutablePoint(-2, 11)],
};

You can omit all but the first use of the const keyword:

1
2
3
4
5
// Only one const, which establishes the constant context.
const pointAndLine = {
'point': [ImmutablePoint(0, 0)],
'line': [ImmutablePoint(1, 10), ImmutablePoint(-2, 11)],
};

If a constant constructor is outside of a constant context and is invoked without const, it creates a non-constant object:

1
2
3
4
var a = const ImmutablePoint(1, 1); // Creates a constant
var b = ImmutablePoint(1, 1); // Does NOT create a constant

assert(!identical(a, b)); // NOT the same instance!

Version note: The const keyword became optional within a constant context in Dart 2.


Getting an object’s type

To get an object’s type at runtime, you can use Object’s runtimeType property, which returns a Type object.

1
print('The type of a is ${a.runtimeType}');

Up to here, you’ve seen how to use classes. The rest of this section shows how to implement classes.

Instance variables

Here’s how you declare instance variables:

1
2
3
4
5
class Point {
double x; // Declare instance variable x, initially null.
double y; // Declare y, initially null.
double z = 0; // Declare z, initially 0.
}

All uninitialized instance variables have the value null.

All instance variables generate an implicit getter method. Non-final instance variables also generate an implicit setter method. For details, see Getters and setters.

1
2
3
4
5
6
7
8
9
10
11
class Point {
double x;
double y;
}

void main() {
var point = Point();
point.x = 4; // Use the setter method for x.
assert(point.x == 4); // Use the getter method for x.
assert(point.y == null); // Values default to null.
}

If you initialize an instance variable where it is declared (instead of in a constructor or method), the value is set when the instance is created, which is before the constructor and its initializer list execute.

Constructors

Declare a constructor by creating a function with the same name as its class (plus, optionally, an additional identifier as described in Named constructors). The most common form of constructor, the generative constructor, creates a new instance of a class:

1
2
3
4
5
6
7
8
9
class Point {
double x, y;

Point(double x, double y) {
// There's a better way to do this, stay tuned.
this.x = x;
this.y = y;
}
}

The this keyword refers to the current instance.


Note: Use this only when there is a name conflict. Otherwise, Dart style omits the this.


The pattern of assigning a constructor argument to an instance variable is so common, Dart has syntactic sugar to make it easy:

1
2
3
4
5
6
7
class Point {
double x, y;

// Syntactic sugar for setting x and y
// before the constructor body runs.
Point(this.x, this.y);
}
Default constructors

If you don’t declare a constructor, a default constructor is provided for you. The default constructor has no arguments and invokes the no-argument constructor in the superclass.

Constructors aren’t inherited

Subclasses don’t inherit constructors from their superclass. A subclass that declares no constructors has only the default (no argument, no name) constructor.

Named constructors

Use a named constructor to implement multiple constructors for a class or to provide extra clarity:

1
2
3
4
5
6
7
8
9
10
class Point {
double x, y;

Point(this.x, this.y);

// Named constructor
Point.origin()
: x = 0,
y = 0;
}

Remember that constructors are not inherited, which means that a superclass’s named constructor is not inherited by a subclass. If you want a subclass to be created with a named constructor defined in the superclass, you must implement that constructor in the subclass.

Invoking a non-default superclass constructor

By default, a constructor in a subclass calls the superclass’s unnamed, no-argument constructor. The superclass’s constructor is called at the beginning of the constructor body. If an initializer list is also being used, it executes before the superclass is called. In summary, the order of execution is as follows:

  • initializer list

  • superclass’s no-arg constructor

  • main class’s no-arg constructor

If the superclass doesn’t have an unnamed, no-argument constructor, then you must manually call one of the constructors in the superclass. Specify the superclass constructor after a colon (:), just before the constructor body (if any).

Because the arguments to the superclass constructor are evaluated before invoking the constructor, an argument can be an expression such as a function call:

1
2
3
4
class Employee extends Person {
Employee() : super.fromJson(defaultData);
// ···
}

Warning: Arguments to the superclass constructor do not have access to this. For example, arguments can call static methods but not instance methods.


Initializer list

Besides invoking a superclass constructor, you can also initialize instance variables before the constructor body runs. Separate initializers with commas (:).

1
2
3
4
5
6
7
// Initializer list sets instance variables before
// the constructor body runs.
Point.fromJson(Map<String, double> json)
: x = json['x'],
y = json['y'] {
print('In Point.fromJson(): ($x, $y)');
}

Warning: The right-hand side of an initializer does not have access to this.


During development, you can validate inputs by using assert in the initializer list.

1
2
3
Point.withAssert(this.x, this.y) : assert(x >= 0) {
print('In Point.withAssert(): ($x, $y)');
}

Initializer lists are handy when setting up final fields. The following example initializes three final fields in an initializer list.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import 'dart:math';

class Point {
final num x;
final num y;
final num distanceFromOrigin;

Point(x, y)
: x = x,
y = y,
distanceFromOrigin = sqrt(x * x + y * y);
}

main() {
var p = new Point(2, 3);
print(p.distanceFromOrigin);
}

Redirecting constructors

Sometimes a constructor’s only purpose is to redirect to another constructor in the same class. A redirecting constructor’s body is empty, with the constructor call appearing after a colon (:).

1
2
3
4
5
6
7
8
9
class Point {
double x, y;

// The main constructor for this class.
Point(this.x, this.y);

// Delegates to the main constructor.
Point.alongXAxis(double x) : this(x, 0);
}

Constant constructors

If your class produces objects that never change, you can make these objects compile-time constants. To do this, define a const constructor and make sure that all instance variables are final.

1
2
3
4
5
6
7
class ImmutablePoint {
static const ImmutablePoint origin = ImmutablePoint(0, 0);

final double x, y;

const ImmutablePoint(this.x, this.y);
}

Constant constructors don’t always create constants. For details, see the section on using constructors - https://dart.dev/guides/language/language-tour#using-constructors.

Factory constructors

Use the factory keyword when implementing a constructor that doesn’t always create a new instance of its class. For example, a factory constructor might return an instance from a cache, or it might return an instance of a subtype. Another use case for factory constructors is initializing a final variable using logic that can’t be handled in the initializer list.

In the following example, the Logger factory constructor returns objects from a cache, and the Logger.fromJson factory constructor initializes a final variable from a JSON object.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Logger {
final String name;
bool mute = false;

// _cache is library-private, thanks to
// the _ in front of its name.
static final Map<String, Logger> _cache =
<String, Logger>{};

factory Logger(String name) {
return _cache.putIfAbsent(
name, () => Logger._internal(name));
}

factory Logger.fromJson(Map<String, Object> json) {
return Logger(json['name'].toString());
}

Logger._internal(this.name);

void log(String msg) {
if (!mute) print(msg);
}
}

Note: Factory constructors have no access to this.


Invoke a factory constructor just like you would any other constructor:

1
2
3
4
5
var logger = Logger('UI');
logger.log('Button clicked');

var logMap = {'name': 'UI'};
var loggerJson = Logger.fromJson(logMap);

Methods

Methods are functions that provide behavior for an object.

Instance methods

Instance methods on objects can access instance variables and this. The distanceTo() method in the following sample is an example of an instance method:

1
2
3
4
5
6
7
8
9
10
11
12
13
import 'dart:math';

class Point {
double x, y;

Point(this.x, this.y);

double distanceTo(Point other) {
var dx = x - other.x;
var dy = y - other.y;
return sqrt(dx * dx + dy * dy);
}
}

Operators

Operators are instance methods with special names. Dart allows you to define operators with the following names:

1
2
3
4
5
<	+	|	[]
> / ^ []=
<= ~/ & ~
>= * << ==
– % >>

Note: You may have noticed that some operators, like !=, are not in the list of names. That’s because they’re just syntactic sugar. For example, the expression e1 != e2 is syntactic sugar for !(e1 == e2).


An operator declaration is identified using the built-in identifier operator. The following example defines vector addition (+) and subtraction (-):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Vector {
final int x, y;

Vector(this.x, this.y);

Vector operator +(Vector v) => Vector(x + v.x, y + v.y);
Vector operator -(Vector v) => Vector(x - v.x, y - v.y);

// Operator == and hashCode not shown.
// ···
}

void main() {
final v = Vector(2, 3);
final w = Vector(2, 2);

assert(v + w == Vector(4, 5));
assert(v - w == Vector(0, 1));
}

Getters and setters

Getters and setters are special methods that provide read and write access to an object’s properties. Recall that each instance variable has an implicit getter, plus a setter if appropriate. You can create additional properties by implementing getters and setters, using the get and set keywords:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Rectangle {
double left, top, width, height;

Rectangle(this.left, this.top, this.width, this.height);

// Define two calculated properties: right and bottom.
double get right => left + width;
set right(double value) => left = value - width;
double get bottom => top + height;
set bottom(double value) => top = value - height;
}

void main() {
var rect = Rectangle(3, 4, 20, 15);
assert(rect.left == 3);
rect.right = 12;
assert(rect.left == -8);
}

With getters and setters, you can start with instance variables, later wrapping them with methods, all without changing client code.


Note: Operators such as increment (++) work in the expected way, whether or not a getter is explicitly defined. To avoid any unexpected side effects, the operator calls the getter exactly once, saving its value in a temporary variable.


Abstract methods

Instance, getter, and setter methods can be abstract, defining an interface but leaving its implementation up to other classes. Abstract methods can only exist in abstract classes.

To make a method abstract, use a semicolon (;) instead of a method body:

1
2
3
4
5
6
7
8
9
10
11
abstract class Doer {
// Define instance variables and methods...

void doSomething(); // Define an abstract method.
}

class EffectiveDoer extends Doer {
void doSomething() {
// Provide an implementation, so the method is not abstract here...
}
}

Abstract classes

Use the abstract modifier to define an abstract class—a class that can’t be instantiated. Abstract classes are useful for defining interfaces, often with some implementation. If you want your abstract class to appear to be instantiable, define a factory constructor.

Abstract classes often have abstract methods. Here’s an example of declaring an abstract class that has an abstract method:

1
2
3
4
5
6
7
// This class is declared abstract and thus
// can't be instantiated.
abstract class AbstractContainer {
// Define constructors, fields, methods...

void updateChildren(); // Abstract method.
}

Implicit interfaces

Every class implicitly defines an interface containing all the instance members of the class and of any interfaces it implements. If you want to create a class A that supports class B’s API without inheriting B’s implementation, class A should implement the B interface.

A class implements one or more interfaces by declaring them in an implements clause and then providing the APIs required by the interfaces. For example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// A person. The implicit interface contains greet().
class Person {
// In the interface, but visible only in this library.
final _name;

// Not in the interface, since this is a constructor.
Person(this._name);

// In the interface.
String greet(String who) => 'Hello, $who. I am $_name.';
}

// An implementation of the Person interface.
class Impostor implements Person {
get _name => '';

String greet(String who) => 'Hi $who. Do you know who I am?';
}

String greetBob(Person person) => person.greet('Bob');

void main() {
print(greetBob(Person('Kathy')));
print(greetBob(Impostor()));
}

Here’s an example of specifying that a class implements multiple interfaces:

1
class Point implements Comparable, Location {...}

Extending a class

Use extends to create a subclass, and super to refer to the superclass:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Television {
void turnOn() {
_illuminateDisplay();
_activateIrSensor();
}
// ···
}

class SmartTelevision extends Television {
void turnOn() {
super.turnOn();
_bootNetworkInterface();
_initializeMemory();
_upgradeApps();
}
// ···
}

Overriding members

Subclasses can override instance methods (including operators), getters, and setters. You can use the @override annotation to indicate that you are intentionally overriding a member:

1
2
3
4
5
class SmartTelevision extends Television {
@override
void turnOn() {...}
// ···
}

To narrow the type of a method parameter or instance variable in code that is type safe, you can use the covariant keyword.


Warning: If you override ==, you should also override Object’s hashCode getter. For an example of overriding == and hashCode, see Implementing map keys - https://dart.dev/guides/libraries/library-tour#implementing-map-keys.


noSuchMethod()

To detect or react whenever code attempts to use a non-existent method or instance variable, you can override noSuchMethod():

1
2
3
4
5
6
7
8
9
class A {
// Unless you override noSuchMethod, using a
// non-existent member results in a NoSuchMethodError.
@override
void noSuchMethod(Invocation invocation) {
print('You tried to use a non-existent member: ' +
'${invocation.memberName}');
}
}

You can’t invoke an unimplemented method unless one of the following is true:

  • The receiver has the static type dynamic.

  • The receiver has a static type that defines the unimplemented method (abstract is OK), and the dynamic type of the receiver has an implemention of noSuchMethod() that’s different from the one in class Object.

For more information, see the informal noSuchMethod forwarding specification - https://github.com/dart-lang/sdk/blob/master/docs/language/informal/nosuchmethod-forwarding.md.

Extension methods

Extension methods are a way to add functionality to existing libraries. You might use extension methods without even knowing it. For example, when you use code completion in an IDE, it suggests extension methods alongside regular methods.

Here’s an example of using an extension method on String named parseInt() that’s defined in string_apis.dart:

1
2
3
4
5
6
7
8
// string_apis.dart

extension NumberParsing on String {
int parseInt() {
return int.parse(this);
}
// ···
}

Use parseInt method:

1
2
3
4
import 'string_apis.dart';
...
print('42'.padLeft(5)); // Use a String method.
print('42'.parseInt()); // Use an extension method.

For details of using and implementing extension methods, see the extension methods page - https://dart.dev/guides/language/extension-methods.

Enumerated types

Enumerated types, often called enumerations or enums, are a special kind of class used to represent a fixed number of constant values.

Using enums

Declare an enumerated type using the enum keyword:

1
enum Color { red, green, blue }

You can use trailing commas when declaring an enumerated type.

Each value in an enum has an index getter, which returns the zero-based position of the value in the enum declaration. For example, the first value has index 0, and the second value has index 1.

1
2
3
assert(Color.red.index == 0);
assert(Color.green.index == 1);
assert(Color.blue.index == 2);

To get a list of all of the values in the enum, use the enum’s values constant.

1
2
List<Color> colors = Color.values;
assert(colors[2] == Color.blue);

You can use enums in switch statements - https://dart.dev/guides/language/language-tour#switch-and-case, and you’ll get a warning if you don’t handle all of the enum’s values:

1
2
3
4
5
6
7
8
9
10
11
12
var aColor = Color.blue;

switch (aColor) {
case Color.red:
print('Red as roses!');
break;
case Color.green:
print('Green as grass!');
break;
default: // Without this, you see a WARNING.
print(aColor); // 'Color.blue'
}

Enumerated types have the following limits:

  • You can’t subclass, mix in, or implement an enum.

  • You can’t explicitly instantiate an enum.

For more information, see the Dart language specification - https://dart.dev/guides/language/spec.

Adding features to a class: mixins

Mixins are a way of reusing a class’s code in multiple class hierarchies.

To use a mixin, use the with keyword followed by one or more mixin names. The following example shows two classes that use mixins:

1
2
3
4
5
6
7
8
9
10
11
class Musician extends Performer with Musical {
// ···
}

class Maestro extends Person
with Musical, Aggressive, Demented {
Maestro(String maestroName) {
name = maestroName;
canConduct = true;
}
}

To implement a mixin, create a class that extends Object and declares no constructors. Unless you want your mixin to be usable as a regular class, use the mixin keyword instead of class. For example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
mixin Musical {
bool canPlayPiano = false;
bool canCompose = false;
bool canConduct = false;

void entertainMe() {
if (canPlayPiano) {
print('Playing piano');
} else if (canConduct) {
print('Waving hands');
} else {
print('Humming to self');
}
}
}

Restrict mixin

Sometimes you might want to restrict the types that can use a mixin. For example, the mixin might depend on being able to invoke a method that the mixin doesn’t define. As the following example shows, you can restrict a mixin’s use by using the on keyword to specify the required superclass:

1
2
3
4
5
6
7
8
9
class Musician {
// ...
}
mixin MusicalPerformer on Musician {
// ...
}
class SingerDancer extends Musician with MusicalPerformer {
// ...
}

In the preceding code, only classes that extend or implement the Musician class can use the mixin MusicalPerformer. Because SingerDancer extends Musician, SingerDancer can mix in MusicalPerformer.

Class variables and methods / Static variables and methods

Use the static keyword to implement class-wide variables and methods.

Static variables

Static variables (class variables) are useful for class-wide state and constants:

1
2
3
4
5
6
7
8
class Queue {
static const initialCapacity = 16;
// ···
}

void main() {
assert(Queue.initialCapacity == 16);
}

Static variables aren’t initialized until they’re used.


Note: This page follows the style guide recommendation - https://dart.dev/guides/language/effective-dart/style#identifiers of preferring lowerCamelCase for constant names.


Static methods

Static methods (class methods) don’t operate on an instance, and thus don’t have access to this. They do, however, have access to static variables. As the following example shows, you invoke static methods directly on a class:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import 'dart:math';

class Point {
double x, y;
Point(this.x, this.y);

static double distanceBetween(Point a, Point b) {
var dx = a.x - b.x;
var dy = a.y - b.y;
return sqrt(dx * dx + dy * dy);
}
}

void main() {
var a = Point(2, 2);
var b = Point(4, 4);
var distance = Point.distanceBetween(a, b);
assert(2.8 < distance && distance < 2.9);
print(distance);
}

Note: Consider using top-level functions, instead of static methods, for common or widely used utilities and functionality.


You can use static methods as compile-time constants. For example, you can pass a static method as a parameter to a constant constructor.

Callable classes

To allow an instance of your Dart class to be called like a function, implement the call() method.

In the following example, the WannabeFunction class defines a call() function that takes three strings and concatenates them, separating each with a space, and appending an exclamation.

1
2
3
4
5
6
7
8
class WannabeFunction {
String call(String a, String b, String c) => '$a $b $c!';
}

var wf = WannabeFunction();
var out = wf('Hi', 'there,', 'gang');

main() => print(out);

Metadata

Use metadata to give additional information about your code. A metadata annotation begins with the character @, followed by either a reference to a compile-time constant (such as deprecated) or a call to a constant constructor.

Two annotations are available to all Dart code: @deprecated and @override. For examples of using @override, see Extending a class. Here’s an example of using the @deprecated annotation:

1
2
3
4
5
6
7
8
9
10
class Television {
/// _Deprecated: Use [turnOn] instead._
@deprecated
void activate() {
turnOn();
}

/// Turns the TV's power on.
void turnOn() {...}
}

You can define your own metadata annotations. Here’s an example of defining a @todo annotation that takes two arguments:

1
2
3
4
5
6
7
8
library todo;

class Todo {
final String who;
final String what;

const Todo(this.who, this.what);
}

And here’s an example of using that @todo annotation:

1
2
3
4
5
6
import 'todo.dart';

@Todo('seth', 'make this do something')
void doSomething() {
print('do something');
}

Metadata can appear before a library, class, typedef, type parameter, constructor, factory, function, field, parameter, or variable declaration and before an import or export directive. You can retrieve metadata at runtime using reflection.

References

[1] Language tour | Dart - https://dart.dev/guides/language/language-tour

[2] Effective Dart: Design | Dart - https://dart.dev/guides/language/effective-dart/design