Dart from the ground up

Classes: values with behaviour

Grouping values with the work that belongs to them, writing a constructor that fills the fields, adding getters that look like values, and overriding toString so printing an object says something useful.

Part 9 of 12

Advanced19 min readBack to Courses

A class keeps related values together

A class describes a kind of thing, and each value made from it is an instance. class Account { ... } names the kind; inside the braces, each final line is a field, one value every account carries. The constructor is the line that fills those fields when a new instance is built.

Account(this.owner, this.cents); is the compact form: each this.name parameter takes an input and stores it in the field of the same name, in one word. final says the value is set once at construction and read afterwards, and int cents; without final says this one is allowed to change.

Behaviour belongs next to its data

A class holds functions too, and they can read the fields without being handed them. void deposit(int amount) => cents += amount; lives inside the class, so a caller writes account.deposit(250) and never has to remember which values travel together.

A getter is a method that is read like a value: bool get overdrawn => cents < 0; is used as account.overdrawn, with no parentheses. Getters are for a small answer you can work out from the fields, so the reader does not have to wonder whether it is stored somewhere or calculated on the spot.

Making an instance print well

Printing an instance of a class shows the class name and a hash-looking number, which is useless in a report. Overriding toString changes what printing shows: String toString() => owner + ": " + cents.toString(); and then print(account) gives the line you want.

The @override line above it is a note to the compiler and to your reader that this method replaces an inherited one. Interpolation uses the same method, so "Account: $account" picks up your text as well — a small touch that pays off the first time you print a list of objects to find out what went wrong.

Named constructors and initialiser lists

A class can have more than one constructor, as long as each has a different name. Item.free(this.name) : qty = 1, cents = 0; is a named constructor: callers write Item.free("Sticker"), and the colon introduces the initialiser list that fills the fields the parameters did not.

The initialiser list runs before the body. That makes it the place for a value worked out from the arguments, or for a field that is final. Account.empty() : owner = "nobody", cents = 0; gives a second way to build the same kind of object without a second class.

A constructor body can do more than fill fields, but it cannot assign a final field that the initialiser list left alone. When you find yourself wanting to, the value probably belongs in the initialiser list, or the field should not be final.

Getters, setters and privacy

A getter is a method read like a field. A setter is the other direction: set cents(int value) { ... } lets a caller write account.cents = 250 and runs your code instead of storing the value directly. That is where a rule such as "never below zero" can live.

A name that starts with an underscore is private to the library it is written in. int _cents; stops code in another file from reading or writing it, while methods in the same file still can. The underscore is a signal and an enforcement at once.

static members belong to the class rather than to an instance: Account.taxRate is read without building an account. Use them for a value or a helper that truly has nothing to do with one object. A static method cannot read the instance fields, because there is no instance to read them from.

A class with a field, a getter and its own text

class Counter {
  Counter(this.label);
  final String label;
  int value = 0;
  void bump() => value++;
  int get doubled => value * 2;
  @override
  String toString() => label + ": " + value.toString();
}

void main() {
  final counter = Counter("visits");
  counter.bump();
  counter.bump();
  print(counter);
  print("doubled: " + counter.doubled.toString());
}

An account that adds up

Run it and watch the summary change after each line. Then add a withdraw method, call it with 900, print the account again, and check that the overdrawn getter now answers true.

Output
Click "Run" to execute your code

Three class mistakes. Declaring a field final and then trying to assign it in a method — final means the constructor is the only writer. Declaring a getter with parentheses, which makes it a method and changes every call that reads it like a value. And printing an object without overriding toString, which shows the class name and a hash rather than the values you wanted to see.

Try these next

  • Add a named constructor Account.empty() and print the object it builds.
  • Turn a public field into a private one with a getter and a setter, and check that callers read it the same way.
  • Add a static counter that goes up in the constructor and print it after building three objects.

A basket of items, with a named constructor

Run it and compare the two baskets the program builds. Then add a second free item with a different name, and give Basket a getter that answers whether it holds more than two items.

Output
Click "Run" to execute your code

When you find yourself passing the same three values into every function, that is the moment to make a class. The values stop being loose and start travelling as one thing.

Check what you learned

One question at a time. A wrong answer explains itself — take another go.

Part 1 of 5

What does Account(this.owner, this.cents) do?