H.org
  • Questions

Dart: Classes

  1. dart
  2. fundamentals

Since Dart is an object-oriented language it supports OOP features like classes and interfaces. Classes are a "blueprint" for creating objects. A class encapsulates state and defines behaviour to mutate that state.

Whenever you work with int, bool or String, these are all data-types. The class keyword allows us to define our own data types for use in our applications.

What's an object?

In object-oriented programming, both data and code is bundled together into a single structure: an object. Dart is a class-based object-oriented language (otherwise known as "class-oriented"), which means the primary mechanism for creating these objects is a class. Once defined, these concrete classes serve as a type that can then be instantiated or inherited from wherever needed.

See Inheritance for more on how inheritance works.

💡 Tip
In Dart, all things that can be assigned to variables are objects and every object is an instance of a class.

Defining a class

Classes usually follow this basic structure:

class ClassName {
// instance variables (fields)
// constructor
// getters/setters
// functions (methods)
}

The class keyword in Dart allows you to define classes:

class XenCrystalSample {
// ...
}

Instance variables

Basic instance variables are next. Also known as fields, these are used for storing state on your class:

final String id;
final String colour;

Constructors

A constructor is a method which is responsible for creating an instance of a class.

The constructor for our XenCrystalSample is simplified because the two properties are marked as final. This means the properties must be initialised.

Here's a slightly modified example using mutable fields:

class XenCrystalSample {
String id;
String colour;

XenCrystalSample(String id, String colour) {
this.id = id;
this.colour = colour;
}
}

XenCrystalSample sample = new XenCrystalSample();

💡 Tip
As of Dart 2, the new keyword for creating instances is optional.

Variable initialisation

TBC

Privacy

Dart has a simplified member privacy model, anything prefixed with an underscore _ is automatically hidden:

class XenCrystalSample {
final String id;
final String colour;
final bool _inserted;

XenCrystalSample(this.id, this.colour);

/// Inserts this [XenCrystalSample] into the anti-mass spectrometer.
bool insert() {
_inserted = true;
}
}

XenCrystalSample testSample = XenCrystalSample('GG-3883', 'green');

🚧 Warning
This does not mean that your source code is hidden. All package source code in Dart is readable, private is simply a visibility modifier to prevent access to specific class members.

Named constructors

Named constructors allow you to provide some or all of the constructor's parameters in a simplified way. This also allows you to define multiple constructors for the same class, each with their own unique name:

class XenCrystalSample {
final String id;
final String colour;

XenCrystalSample(this.id, this.colour);

XenCrystalSample.cascade()
: this.id = 'GG-3883',
this.colour = 'green';

XenCrystalSample.unknown()
: this.id = 'EP-0021',
this.colour = 'purple';
}

XenCrystalSample cascade = XenCrystalSample.cascade();
XenCrystalSample unknown = XenCrystalSample.unknown();

Getters/setters

These allow you to define custom behaviour for when a variable is accessed (get) and mutated (set). You can define a get, set or both, but if you don't provide any, Dart will create implicit getters/setters for you.

Notice the _id instance variable in the example? The getter and setter use this private backing field to act as storage:

class XenCrystalSample {
String _id;

XenCrystalSample(String id) {
this.id = id;
}

String get id {
// custom get behaviour
return _id;
}

void set id(String id) {
// custom set behaviour
_id = id;
}
}

💡 Tip
In some languages like C# and Java, it's common to hide all fields behind getters/setters. This is because calling a field and calling getters/setters is different. If you ever have to make a change to those fields, you will need to modify all the places where that field is accessed.

Dart is different: getters/setters and fields are identical, so you don't need to follow this practice. If you just wrap all of your instance variables with getters/setters, the linter will produce a warning:

Avoid wrapping fields in getters and setters just to be "safe"

Functions

Functions that are attached to a class are called methods. This is where you model the behaviour your class exhibits.

We've already seen an example of this previously in this article: the insert() method. This changes the internal state of the XenCrystalSample to indicate it is currently inserted:

class XenCrystalSample {
// ...

final bool _inserted;

/// Inserts this [XenCrystalSample] into the anti-mass spectrometer.
bool insert() {
_inserted = true;
print('Sample $id inserted');
// perform other actions relevant to sample insertion
}
}

XenCrystalSample testSample = XenCrystalSample('GG-3883', 'green');
testSample.insert();
Sample GG-3883 inserted

Converting objects to strings

Classes in Dart have a toString() method provided by the base Object class. Unfortunately the default value returned is particularly unhelpful:

Instance of 'XenCrystalSample'

You should always override the toString() method in order to describe what an instance of the class contains:

@override
String toString() {
return "A $colour Xenium crystal sample '$id'";
}
A green Xenium crystal, sample 'GG-3883'

Abstract classes

Abstract classes cannot be instantiated directly and must be subclassed by a concrete class in order to use. In this example, XenCrystalSample is a concrete subclass:

abstract class Sample {
/// Inserts this [XenCrystalSample] into the anti-mass spectrometer.
void insert();
}

class XenCrystalSample extends Sample {
// ...

@override
bool insert() {
_inserted = true;
print('Sample $id inserted');
// perform other actions relevant to sample insertion
}
}

By omitting the body of a method in an abstract class you are instructing Dart that this method is also abstract and must be defined in a subclass.

Marking the insert() method with the @override annotation indicates you intentionally meant to override the method:

class XenCrystalSample extends Sample {
// ...

@override
bool insert() {
// implementation
}
}

📜 Further reading
I'll be going into much more detail about inheritance in a future article.

Interfaces

Interfaces define a contract between two object, in fact some developers refer to interfaces as "contracts". Every class that you create in Dart defines an implicit interface. In other languages, this would be something you have to explicitly define.

This means that any class can implement another class' interface using implements:

class XenCrystalSample implements Sample {
void insert() {
// implementation
}
}
Ko-Fi button Support Me on Ko-fi

Created with lots of green tea and 11ty

Opinions are solely my own and not those of my employer.

Privacy Policy - Licenses