Dart

Dart Classes

Module Still Under Development

# 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();
1
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;
  }
}
1
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');
1
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);
  }
}
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
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;
  }
}
1
2
3
4
5
6

write:

class Cat extends Animal {
  String name;
  Cat(this.name);
}
1
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);
}
1
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');
1
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
}
1
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;
  }
}
1
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
1
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){
    //...
  }
}
1
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);
}
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
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);
}
1
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 );
1
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
  }
}
1
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);
}
1
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 implemented.

# Hybrid Modules

Last Updated: 11/6/2023, 9:09:27 AM