Dart: Classes
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, thenew
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:
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 {
// ...
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 {
// ...
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
}
}