Classes in Dart

By Syncfusion®, Inc. · Last updated

How to design classes with fields and methods, make objects, use this, static members and cascades, and build objects with default, named, redirecting, const and factory constructors.

What Are Classes and Objects?

Class = a blueprint that describes a kind of thing: what it has and what it can do.

Object = one real thing made from that blueprint, with its own values.

For example:

  • the class Student says every student has a name and an age, and can introduce themselves
  • the object Asha, 13 is one student
  • the object Ravi, 14 is another student, made from the same class

Simple lesson: a class is the plan, and objects are the things built from the plan.

A Real-World Example

A school uniform shop has one design for the school shirt.

  • The design says: a collar, a pocket, a school badge.
  • The shop makes many shirts from that one design.
  • Each shirt has its own size and its own name tag.

The design is the class. Each shirt is an object.

Every Run box on this page runs on the course's Dart runner in your browser.

It supports classes with fields, methods, constructors and static members. Inheritance comes later, in Inheritance.

What You Will Learn

In this lesson, you will learn:

  1. What classes and objects are, and how to write them.
  2. Fields, methods, this, static members and the cascade operator.
  3. Default and parameterized constructors.
  4. Named constructors, initializer lists and redirecting constructors.
  5. Const and factory constructors.

Object Fundamentals

Everything Is an Object

In Dart, every value is an object, even numbers and text.

Root type = Object, the type at the top. Every class is a kind of Object.

Numbers and text are objects too

void main() {
  print(5.isEven);
  print('Dart'.length);
  print(5 is Object);
  print('Dart' is Object);
  print([1, 2].runtimeType);
}

isEven and length are members of those objects. A dot (.) reaches a member.

Object Members

Every object in Dart has these, because they come from Object:

MemberWhat it does
toString()gives the object as text. print calls it.
hashCodea number used by sets and maps to store the object
==checks whether two objects are equal
runtimeTypethe object's type, right now
noSuchMethod()runs when a missing member is called on a dynamic value

You can change how toString() and == behave in your own classes (see below).

Simple lesson: every Dart value is an object, and every object has toString(), hashCode and ==.

Class Fundamentals

Class Structure

class ClassName {
  // fields: what each object HAS
  // constructor: how to MAKE an object
  // methods: what each object can DO
}

Class Declaration

Your first class

class Student {
  String name;
  int age;

  Student(this.name, this.age);

  void introduce() {
    print('Hi, I am $name and I am $age years old.');
  }
}

void main() {
  var asha = Student('Asha', 13);
  var ravi = Student('Ravi', 14);
  asha.introduce();
  ravi.introduce();
}
  • String name; and int age; = fields. Every Student has them.
  • Student(this.name, this.age); = the constructor. It puts the values into the fields (see constructors, below).
  • void introduce() = a method. Every Student can do it.

Class Naming Conventions

  • Class names use UpperCamelCase: Student, ReportCard, LibraryBook.
  • Use a noun (a thing), in the singular: Student, not Students.

Make objects from the class, then click one to call its method:

Simple lesson: a class groups fields and methods under one UpperCamelCase name.

Objects

Creating Objects

Instance = another word for an object made from a class.

  • Call the class like a function: Student('Asha', 13).
  • In old Dart code you may see new Student(...). The new is optional now.

Accessing Object Members

Use a dot: asha.name, asha.age = 14, asha.introduce().

Reading and changing fields

class Student {
  String name;
  int age;

  Student(this.name, this.age);
}

void main() {
  var asha = Student('Asha', 13);
  print(asha.name);
  print(asha.age);
  asha.age = 14;
  print('After her birthday: ${asha.age}');
}

Object References

Reference = a variable does not hold the object itself. It holds where the object is.

Two variables can point to the same object:

Two names, one object

identical(a, b) = true only if both point to the very same object.

Passing an object to a function also passes the reference.

If the function changes the object's fields, the caller sees the change too. Make a new object when you need a separate copy.

Simple lesson: a variable holds a reference, so two variables can share one object.

Instance Variables

Instance variable (field) = a variable that belongs to each object. Every object has its own copy.

Type Annotations and Default Values

Fields with types and starting values

class ReportCard {
  String studentName;
  int maths = 0;
  int science = 0;
  String? remark;

  ReportCard(this.studentName);
}

void main() {
  var card = ReportCard('Priya');
  print(card.maths);
  print(card.remark);
  card.maths = 91;
  card.remark = 'Excellent';
  print('${card.studentName}: ${card.maths}, ${card.remark}');
}
  • int maths = 0; = every new card starts with 0.
  • String? remark; = may be empty, so it starts as null (see Null Safety).
  • A non-nullable field with no value must be set by the constructor, or Dart refuses the class.

Simple lesson: each object has its own fields, and each field needs a starting value.

Methods

Instance method = a function that belongs to a class, and works with that object's fields.

Method Parameters and Return Values

Methods that take inputs and give answers

class ReportCard {
  String name;
  int maths;
  int science;

  ReportCard(this.name, this.maths, this.science);

  int total() {
    return maths + science;
  }

  bool hasPassed(int passMark) => maths >= passMark && science >= passMark;

  void addBonus(int marks) {
    maths += marks;
  }
}

void main() {
  var card = ReportCard('Kiran', 72, 30);
  print(card.total());
  print(card.hasPassed(35));
  card.addBonus(5);
  print(card.maths);
}
  • Methods use the object's fields directly: maths + science.
  • They can take parameters and return values, like any function (see Functions).

Overriding toString()

By default, printing an object shows Instance of 'ClassName'. Give it a better toString():

A better toString

class Student {
  String name;
  int age;
  Student(this.name, this.age);
}

class Teacher {
  String name;
  String subject;
  Teacher(this.name, this.subject);

  @override
  String toString() => 'Teacher($name, $subject)';
}

void main() {
  print(Student('Asha', 13));
  print(Teacher('Mrs. Rao', 'Maths'));
}

@override = a note that says "this replaces a member I already had" (here, from Object).

Simple lesson: methods are what an object can do, and toString() decides how it prints.

The this Keyword

this = the current object, the one the method was called on.

Resolving Naming Conflicts

When a parameter has the same name as a field, the parameter wins. Use this. to reach the field:

this. picks the field

class Student {
  String name;
  int age;

  Student(this.name, this.age);

  void rename(String name) {
    this.name = name;
  }

  void describe() {
    print('${this.name} is $age');
  }
}

void main() {
  var s = Student('Ashu', 13);
  s.rename('Asha');
  s.describe();
}
  • In rename, name alone means the parameter. this.name means the field.
  • In describe, there is no clash, so age and this.age mean the same. Dart style leaves this. out when it is not needed.

Simple lesson: use this. only when a parameter hides a field with the same name.

Static Members

Static member = a field or method that belongs to the class itself, not to each object.

Instance or static?

Static Variables and Static Methods

Counting students with a static field

class Student {
  static int count = 0;
  static const schoolName = 'Green Valley';

  String name;

  Student(this.name) {
    count++;
  }

  static String welcome() => 'Welcome to $schoolName!';
}

void main() {
  print(Student.welcome());
  print(Student.count);
  Student('Asha');
  Student('Ravi');
  Student('Priya');
  print(Student.count);
}

Accessing Static Members

  • Outside the class, write Student.count and Student.welcome().
  • Inside the class, you can write just count.
  • A static method cannot use instance fields like name, because it does not belong to any one student.

Simple lesson: static members belong to the class and are shared, instance members belong to each object.

The Cascade Operator

Cascade (..) = do several things to the same object, one after another, in one expression.

Setting up an object with cascades

class ReportCard {
  String name = '';
  int maths = 0;
  int science = 0;

  void show() {
    print('$name: Maths $maths, Science $science');
  }
}

void main() {
  var card = ReportCard()
    ..name = 'Meena'
    ..maths = 88
    ..science = 93
    ..show();
  print(card.maths + card.science);
}
  • Each .. works on the object made by ReportCard().
  • The whole cascade gives back that object, so card gets it.
  • Method chaining = calling one method after another in a single expression. Cascades let you chain even methods that return nothing.

The Null-Aware Cascade (?..)

?.. = start the cascade only if the object is not null.

?.. skips a null object

class ReportCard {
  String name = '';
  void show() => print('Card for $name');
}

void main() {
  ReportCard? missing;
  missing?..name = 'Nobody'..show();
  var found = ReportCard()..name = 'Zoya';
  found..show();
}

Easy way to remember

  • class = the cookie cutter. object = each cookie.
  • field = what it has. method = what it does.
  • static = one shared copy for the whole class.
  • .. = "and also do this, to the same object".

Simple lesson: .. sets up one object step by step, and ?.. does nothing if the object is null.

What Is a Constructor?

Constructor = a special function inside a class that builds a new object and sets up its fields.

  • It has the same name as the class.
  • It runs once, when the object is made.
  • A class can have several constructors, for different ways of building.

For example, a Student can be built:

  • from a name and an age
  • as a "guest" with default values
  • from a line of text like 'Ravi,14'

Simple lesson: a constructor is the recipe Dart follows to build each new object.

A Real-World Example

A school issues ID cards in different ways.

  • A new student fills in a form with name and class.
  • A visitor gets a ready-made "Guest" card.
  • A transfer student brings an old card, and the office copies the details.

Three ways in, but every result is an ID card. Pick a constructor to see what it builds:

Some rules on this page are checked by real Dart before the program runs. Those are shown as plain code.

Default Constructors

Default constructor = the constructor Dart gives you when you write none. It takes no inputs.

No constructor written, but objects can still be made

class Bell {
  int rings = 0;

  void ring() {
    rings++;
    print('Ring! ($rings)');
  }
}

void main() {
  var bell = Bell();
  bell.ring();
  bell.ring();
}

Every field needs a starting value, because nothing sets it up.

Simple lesson: if you write no constructor, Dart adds an empty one for you.

Parameterized Constructors

Parameterized constructor = a constructor that takes inputs and puts them into fields.

The Long Way

class Student {
  late String name;
  late int age;

  Student(String name, int age) {
    this.name = name;
    this.age = age;
  }
}

The Short Way: this.field

Initializing formal = a parameter written this.name. It puts the argument straight into the field.

this.field does the work for you

class Student {
  String name;
  int age;

  Student(this.name, this.age);
}

void main() {
  var s = Student('Asha', 13);
  print('${s.name}, ${s.age}');
}

Student(this.name, this.age); does the same as the long way, in one line. This is the Dart style.

Named and Optional Parameters

Constructors can use every parameter style from Functions:

Named, required and default values in a constructor

class Student {
  String name;
  int age;
  String house;

  Student({required this.name, required this.age, this.house = 'Not chosen'});
}

void main() {
  var a = Student(name: 'Asha', age: 13, house: 'Tagore');
  var b = Student(age: 12, name: 'Kiran');
  print('${a.name}: ${a.house}');
  print('${b.name}: ${b.house}');
}

Simple lesson: this.field parameters fill fields directly, in any parameter style.

Named Constructors

Named constructor = an extra constructor with its own name: ClassName.name(...).

  • Use it for a different way of building the same kind of object.
  • The name explains the way: Student.guest(), Student.fromText(...).

A second way to build a Student

class Student {
  String name;
  int age;

  Student(this.name, this.age);

  Student.guest()
      : name = 'Guest',
        age = 0;

  @override
  String toString() => '$name ($age)';
}

void main() {
  print(Student('Asha', 13));
  print(Student.guest());
}

Student.guest() sets the fields itself, with an initializer list (the part after :).

Dart has no "overloading": two constructors cannot share a name, even with different inputs.

Named constructors solve this. Each way of building gets its own clear name.

Simple lesson: a named constructor is a second, clearly named recipe for the same class.

Constructor Initializer Lists

Initializer list = the part after : that sets fields before the constructor body runs.

  • Items are separated by commas.
  • It can use the parameters, but not other methods of the object.
  • It is the right place to set final fields from a calculation.

An initializer list works out a final field

A final field must get its value before the body runs. In real Dart, setting it inside { } is an error:

class Rectangle {
  final int area;
  Rectangle(int w, int h) {
    area = w * h;
  }
}

Dart says Final field 'area' is not initialized. Use an initializer list instead.

Simple lesson: an initializer list sets fields early, which is how final fields get computed values.

Redirecting Constructors

Redirecting constructor = a constructor that just hands the work to another constructor with : this(...).

A newcomer is a Student aged 11

class Student {
  String name;
  int age;

  Student(this.name, this.age);

  Student.newcomer(String name) : this(name, 11);

  @override
  String toString() => '$name ($age)';
}

void main() {
  print(Student.newcomer('Kiran'));
  print(Student('Zoya', 12));
}
  • No body is allowed. The other constructor does everything.
  • It avoids writing the same set-up code twice.

Simple lesson: a redirecting constructor passes its inputs on to another constructor.

Constant Constructors

Constant constructor = a const constructor. It lets you make compile-time constant objects (see Variables and Constants).

Rules:

  • Every field must be final.
  • The call uses const: const Point(2, 3).

A const constructor

class Point {
  final int x;
  final int y;

  const Point(this.x, this.y);

  @override
  String toString() => '($x, $y)';
}

void main() {
  const start = Point(0, 0);
  var finish = Point(4, 3);
  print(start);
  print(finish);
}

In real Dart, two const Point(0, 0) values are the very same object, so they share memory. This makes Flutter apps faster, because unchanged parts of the screen are built once.

Simple lesson: a const constructor makes objects that are fixed forever, and Dart can share them.

Factory Constructors

Factory constructor = a constructor marked factory. It runs code, then returns an object.

  • It can return a new object, or an existing one.
  • It is good for building from other data, like text or a map.

Normal constructor or factory?

Building from text with a factory

class Student {
  String name;
  int age;

  Student(this.name, this.age);

  factory Student.fromText(String text) {
    var parts = text.split(',');
    return Student(parts[0].trim(), int.parse(parts[1].trim()));
  }

  @override
  String toString() => '$name ($age)';
}

void main() {
  var lines = ['Asha, 13', 'Ravi, 14'];
  for (var line in lines) {
    print(Student.fromText(line));
  }
}

Returning an Existing Object

A factory can keep a cache and hand back the same object again:

One ID card per student, never two

class IdCard {
  static final _issued = <String, IdCard>{};
  final String holder;

  IdCard._(this.holder);

  factory IdCard(String holder) {
    return _issued.putIfAbsent(holder, () => IdCard._(holder));
  }
}

void main() {
  var first = IdCard('Asha');
  var again = IdCard('Asha');
  var other = IdCard('Ravi');
  print(identical(first, again));
  print(identical(first, other));
}
  • IdCard._(...) is a private named constructor (see Libraries). Only this file can call it.
  • The factory gives back the card that was already issued.

You will use factories a lot in File Handling, as fromJson.

Easy way to remember

  • this.x = fill the field from the argument.
  • .name(...) = another way to build.
  • : x = ... = set fields before the body.
  • : this(...) = let another constructor do it.
  • const = fixed forever. factory = run code, then return an object.

Simple lesson: a factory decides which object to return, so it can reuse objects or build them from data.

TL;DR

  1. A class is a blueprint. An object is one thing made from it: Student('Asha').
  2. Fields hold data. Methods do work. this means the current object.
  3. static members belong to the class. .. (cascade) does many things to one object.
  4. Constructors: Student(this.name);, named Student.guest(), initializer lists : x = 1, redirecting : this(...).
  5. const constructors make fixed objects. factory constructors can return an existing or a different object.

Common mistakes

MistakeWrong ideaCorrect ideaWhat goes wrong
class studentAny case is fineClass names use UpperCamelCaseIt works, but every Dart reader is confused.
A non-nullable field with no valueFields start emptySet it in the constructor, or give a defaultField must be initialized.
b = a, then expecting a copy= copies the object= copies the referenceChanging b also changes a.
name = name; in a methodIt sets the fieldThe parameter hides the fieldThe field never changes. Use this.name.
asha.count for a static fieldStatics belong to objectsUse Student.countAn error.
A static method using nameStatic methods see fieldsThey belong to no single objectAn error.
Printing an object without toStringIt prints its fieldsIt prints Instance of 'Student'Unhelpful output.
A non-nullable field with no valueIt starts emptySet it in the constructor or give a defaultNon-nullable field must be initialized.
Setting a final field in the bodyThe body runs firstUse this.x or an initializer listFinal field is not initialized.
name = name; in a constructor bodyIt sets the fieldThe parameter hides the fieldUse this.name = name;, or this.name in the parameters.
A body on a redirecting constructorIt can add more codeIt must only redirectA compile error.
A const constructor with a non-final fieldconst makes fields finalEvery field must be finalA compile error.
Using this in a factoryA factory has an objectNo object exists yetA compile error.
Two constructors with the same nameDart picks by inputsEvery constructor needs its own nameThe constructor is already defined.

Check Yourself

Exercises

What to Remember

  • A class is a blueprint, and objects are made from it.
  • Fields store data, methods do work, and this is the current object.
  • Static members belong to the class, not to each object.
  • this.x in a constructor saves a value straight into a field.
  • Use named, const and factory constructors when one plain constructor is not enough.

Next: Enums. You will learn to describe a fixed set of choices, like the days of the week or the school houses.