Dart Classes
# Classes
Unlike JavaScript, Dart uses actual object oriented classes for the majority of the code that you will write.
If you are still not clear on what a class is, the simplest description is that a class is a blueprint for any kind of object. Eg: A Student
class would define objects that could have properties
called name
, program
, GPA
, and course_list
. It could also have functions called attend_class
, do_homework
, ask_question
, and graduate
.
The properties in a function we will call member fields
. The functions we will call member methods
.
When you create an object of the type defined by the class, you are running the constructor function, and it will return an instance
of the class. Instantiation
is the name for the process of
creating an object of a specific type.
The name of classes should start with a capital letter. If the class name is made from multiple words then use camel-case.
Most classes will have a constructor method. The default constructor will have the same name as the class itself. If you don't add one, then Dart will automatically create an empty one for you.
class Something {
Something() {
//default constructor method has the same name as the class
//constructors do NOT have a return type
}
}
//call the constructor method to create an object of type Something
Something s = Something();
2
3
4
5
6
7
8
9
Sometimes a class will have a member field or method that belongs to the class, not to the instance. For example, let's say you wanted to count the number of students that have been instantiated. So,
inside the constructor method we would increment a variable that tracks the number of times it has been called. The count variable can't belong to a single instance. It needs to remain outside of all
the instances, in the class itself. These member fields and methods are called static
.
class Student {
static int count = 0;
String name;
int id;
Student(String _name, int _id){
//increment the static count
Student.count++;
//create the student object with it's member fields
this.name = _name;
this.id = _id;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
# Named Constructors
If you want, you can also create alternate constructors called named constructors. These would be used to create objects of the same type when you want to be able to pass different parameters to the constructor.
class Apple {
Apple(){
//default constructor
}
Apple.red(String type){
//named constructor
}
Apple.green(String type){
//named constructor
}
}
Apple a1 = Apple();
Apple a2 = Apple.red('delicious');
Apple a3 = Apple.green('granny smith');
2
3
4
5
6
7
8
9
10
11
12
13
14
15
The same rules for function parameters apply to constructor parameters.
# Extending Classes
Classes in Dart, like classes in other languages, can be extended. This means that there is a more general type of object that creates a foundation for the class. As an example an Animal
object is
more general, less specific than say Cat
. Beyond that, Tabby
is even more specific than Cat
. So, in code we would declare three classes where Cat
extends Animal
and Tabby
extends Cat
.
class Animal {
int numLegs = 2; //default value
Animal(); //default constructor can use ; instead of { }
void eat(String food){
}
}
class Cat extends Animal {
int numLegs = 4;
String name;
Cat(String name){
//super(); will automatically be called by Dart to call Animal()
//If Animal() needed parameters then you need to call it explicitly
//or if Animal has a non-default constructor you need to call super()
this.name = name;
//use of `this` only used to avoid name conflict
//Dart style omits the keyword this whenever possible
}
}
class Tabby extends Cat {
Tabby(String name){
super(name);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
# Constructor Shorthand
In the Cat
class there is another way that we could write the constructor which is much simpler. If all you need to do in your constructor is pass a value into a member field then, instead of:
class Cat extends Animal {
String name;
Cat(String name){
this.name = name;
}
}
2
3
4
5
6
write:
class Cat extends Animal {
String name;
Cat(this.name);
}
2
3
4
By putting this.name
into the parentheses you are accepting a value whose type matches the type of name
and automatically assigning it to the variable.
In the case of the Tabby
class, we do not need to declare a member field called name because it was already declared in Cat
which is what Tabby
extends. When a sub-class extends a parent-class
(or super-class) then the member fields are already available because we call super()
.
If a value needs to be passed to a super()
call, we can still shorten it.
class Tabby extends Cat {
Tabby(String name): super(name);
}
2
3
With the three classes we can call their constructors, instantiate, and use them like this:
Animal a = Animal();
print( a.numLegs ); //2
a.eat('Nachos');
Cat c = Cat('Zazzles');
print( c.numLegs ); //4
c.eat('Fish');
Tabby t = Tabby('Oppenheimer');
print( t.numLegs ); //4
t.eat('Lasagna');
2
3
4
5
6
7
8
9
10
11
# This
The Dart keyword this
refers to the current instance of your class. The Dart convention is that you avoid using the keyword as much as possible. We only add it when needed to avoid a naming conflict
with another variable.
class Thing{
String type = 'cheese'; // member field
Thing(){
this.type = 'cheese';
type = 'cheese';
//The above two lines are doing the same thing
//the convention is to leave `this` off unless needed
} //constructor
}
2
3
4
5
6
7
8
9
10
# @override
When you have a subclass which is extending a superclass, if there is a method in the superclass, then the subclass has access to it.
However, if you want to change how the method works in some way, then you can override the method by declaring a function with the same name, prefixed with @override
.
The new version of the method must:
- have the same return type.
- have the same number of positional parameters.
- parameters need to have the same types.
class Thing {
Thing();
int someFunc(int val){
return val;
}
void message(String msg){
print(msg);
}
}
class BigThing extends Thing {
BigThing();
int someFunc(int val){
return val * 10000;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
If you want to protect your code and handle attempted calls to methods that were missing and not overridden, then you can use the built-in method name noSuchMethod
.
Let's say that our thing
class above had a method called message()
. Then, the BigThing
class didn't override the method.
BigThing bt = BigThing();
bt.someFunc(7); // 70000
bt.message('hola amigo'); // this method does not exist in BigThing class
2
3
So, when we call bt.message('hola amigo')
, it still works because the default call to BigThing()
will automatically call super()
which will give us access to the member fields and methods from
Thing
.
# Implements Interface
When you extend a class you will automatically get access to the member fields and methods from the superclass.
You are also allowed to use @override
to change the value of a field or change the functionality of a method.
If, instead of extends
you write implements
it means that you will be required to define every method and every field from the class that you are implementing.
By default, every Dart class that you write will implicitly define an interface. An interface is just a list of properties and methods. An interface does not have to be instantiated, it is just the
list of properties and methods that another class must include if it implements
the class interface.
class Animal {
//Animal is a class and also an interface
String type;
Animal(); //the constructor is not part of the interface
void eat(String food){}
}
class Dog implements Animal{
String type;
//because Dog implements Animal, it MUST have a type field and an eat method
Dog(this.type='dog');
void eat(String food){
//...
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Notice that the Dog class does not use the keyword @override
because it isn't changing the property or the method, it is just required to have them inside itself.
When you use extends
on your class definition, you are only allowed to extend a single superclass.
When you use implements
on your class definition, you are allowed to implement as many class interfaces as you want. Just separate the names with commas.
# Mixins
Mixins are similar to Interfaces. They are a way to add reusable code to your class. They use the keyword with
, instead of implements
or extends
. You can add as many of them as you want. Just
separate the names of the mixins with commas.
Conceptually you can think of them like CSS Classes. You assemble a bunch of properties with values as a collective style. Then you can add that style to any HTML element, regardless of the type of element or where it is in your webpage.
Mixins can include a list of fields and methods. They can NOT use extends
.
mixin Drums {
bool hasTom = true;
bool hasSnare = true;
bool hasBass = true;
bool hasCymbals = true;
int numSticks = 2;
void addDrumStick(int sticks=1){
numSticks += sticks;
}
}
mixin Keyboards {
bool hasAcoustic = false;
bool hasElectric = true;
}
class Band with Drums, Keyboards {
bool hasLeadSinger = true;
int numBackUpSingers = 0;
int numGuitarists = 1;
int numBassists = 0;
String name;
Band(this.name);
}
class RockBand extends Band {
int numBassists = 1;
RockBand(String name): super(name);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
# Getters and Setters
Getters and setters are special methods that provide read and write access to an object’s properties. 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:
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);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
In the above Rectangle
class, there are 4 declared double
variables top
, left
, width
and height
. When you call the constructor, you pass in those 4 numbers.
The variables right
and bottom
are created not just as a double
with a value, they also have getter and setter methods.
The get
methods are used when you try to access the values.
//add this line inside the main() function
print( right );
2
It looks just like you are accessing the value of any old variable. What happens though is the get right
arrow function is called and Dart calculates the value of right
.
The set
methods are used to change the value inside of right
and bottom
. When you pass a new value to the set right
or set bottom
function, Dart actually calculates a new value for left
or
top
based on the value passed to the function.
get right
, set right
, get bottom
, and set bottom
are not variables that hold a value, they are functions that can modify or read values of other variables inside the current class.
# Initializer Lists
Sometimes, when you instantiate a class, by calling its constructor method, you might want to do some work on the parameters being passed to the constructor. The code that you run to do that is called
an initializer list
. This code is placed between the ()
and the opening {
of the constructor. It starts with :
and is a comma separated list of statements.
class Student {
String name;
int id;
Student(this.name, this.id): assert(name.length > 1), assert(id > 0){
//makes sure that string length greater than 1 and integer is greater than 0
}
}
2
3
4
5
6
7
8
The assert()
method is kind of like an if statement except it throws an exception if it fails and just lets the code continue if it works.
class MyNumbers {
int min;
int max;
MyNumbers(String x, String y):
min = int.parse(x),
max = int.parse(y)
{
//we now have the x and y string values
//converted from strings to integers
//and saved in the variables min and max
print((min * max).toString() );
}
}
void main(){
MyNumbers mn = MyNumbers('42', '99');
print(mn.min, mn.max);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
In the second example we convert the two strings passed to the constructor and assign them to member fields min
and max
. All that happens BEFORE the constructor function runs.
# Class Modifiers
The mixin
and interface
keywords discussed above are two of the class modifiers available in Dart. The other modifiers are abstract
, final
, base
, and sealed
.
Full reference for class modifiers (opens new window)
These modifiers are all basically controlling what a subclass is allowed to do with the methods and fields from the parent/super class.
The mixin class
is a way for a subclass to be able to use methods from multiple parent classes. Basically it creates a way to simulate multiple inheritance.
The abstract
and interface
classes are similar. Both create a class that you would never instantiate. Only the child/sub class gets instantiated and it will get all the methods from the parent.
A final class
is one that you can't extend.
A sealed class
is one that you can extend but you can't override and change the methods that are inherited.
Any class that extends a base
class must be marked as base
, final
, or sealed
. If a base class has private members, they will exist in the subclass too. A base
class can be extended but
cannot be implement
ed.