Dart Classes and Objects

A class is a core concept of object-oriented programming; it is an abstract description of things in the real world.

Objects are concrete instances of classes. This chapter introduces class definitions, constructors, member variables and methods, getters/setters, and static members in Dart.


Class definition and instantiation

A class is a template for creating objects, defining what properties (data) and behaviors (methods) the objects have.

Example

// Define a User class
class User {
  // Member variables (properties)
  String name = '';
  int age = 0;

  // Member methods (behaviors)
  void introduce() {
    print('Hello, I am $name, I am $age years old.');
  }

  bool isAdult() {
    return age >= 18;
  }
}

void main() {
  // Instantiate: with the new keyword (new can be omitted in Dart 2.0+)
  var user1 = User();
  user1.name = 'example';
  user1.age = 10;

  // Access member variables and methods
  print('User name: ${user1.name}');
  user1.introduce();
  print('Is adult: ${user1.isAdult()}');

  // Create another object, independent of each other
  var user2 = User();
  user2.name = 'Xiao Ming';
  user2.age = 20;
  user2.introduce();
}
用户名: example
你好,我是 example,今年 10 岁。
是否成年: false
你好,我是 小明,今年 20 岁。

Since Dart 2.0, the `new` keyword is optional when instantiating. The community convention is to omit `new` to keep code concise. Subsequent examples in this tutorial will omit `new`.


Constructor

A constructor is a special method automatically called when an object is created, used to initialize the object's state.

Dart provides multiple forms of constructors, making them very flexible.

Default constructor

If you do not define a constructor, Dart automatically provides a no-argument default constructor.

Regular constructor

Example

class Student {
  String name;
  int age;

  // Normal constructor: same name as the class
  Student(String name, int age) {
    this.name = name;
    this.age = age;
  }

  void printInfo() {
    print('Student: $name, Age: $age');
  }
}

void main() {
  var s1 = Student('example', 10);
  var s2 = Student('Xiao Ming', 15);
  s1.printInfo();
  s2.printInfo();
}
学生: example, 年龄: 10
学生: 小明, 年龄: 15

Syntactic sugar constructor

When constructor parameters are directly assigned to member variables, Dart provides shorthand syntax.

Example

class Student {
  String name;
  int age;

  // Syntactic sugar: this.parameterName automatically assigns the parameter to a member variable with the same name
  // This way is more concise than manually writing this.name = name
  Student(this.name, this.age);

  void printInfo() {
    print('EXAMPLE Student: $name, Age: $age');
  }
}

void main() {
  var s = Student('example', 10);
  s.printInfo();
}
EXAMPLE 学生: example, 年龄: 10

Named constructor

A class can have multiple named constructors for different initialization logic.

Example

class Point {
  double x;
  double y;

  // Default constructor
  Point(this.x, this.y);

  // Named constructor: creates the origin
  Point.origin()
      : x = 0,
        y = 0;

  // Named constructor: creates from a single value (x and y are the same)
  Point.diagonal(double value)
      : x = value,
        y = value;

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

void main() {
  var p1 = Point(3, 4);
  var p2 = Point.origin();
  var p3 = Point.diagonal(5);

  print('EXAMPLE Coordinates: $p1, $p2, $p3');
}
EXAMPLE 坐标: Point(3.0, 4.0), Point(0.0, 0.0), Point(5.0, 5.0)

initializer list

The initializer list runs before the constructor body executes and is used to initialize final fields or perform parameter validation.

Example

class Person {
  final String name;    // final fields must be initialized in the constructor
  final int birthYear;
  final int age;

  // Initializer list: after the colon, before the function body
  // The initializer list runs before the constructor body executes
  Person(this.name, this.birthYear)
      : age = 2026 - birthYear,          // Computed property
        assert(birthYear > 1900, 'Invalid birth year');  // Assertion check

  void printInfo() {
    print('EXAMPLE User: $name, Birth: $birthYear, Age: $age');
  }
}

void main() {
  var p = Person('example', 2016);
  p.printInfo();

  // The following line will trigger an assert failure (in development mode)
  // var p2 = Person('error', 1800);
}
EXAMPLE 用户: example, 出生: 2016, 年龄: 10

`assert` only works in debug mode and is ignored in release mode. It is used to detect logic errors early during development.


Member variables and methods

Member variables store the state of an object, and member methods define the behavior of an object.

Example

class BankAccount {
  // Private member: starts with an underscore, accessible only in this file
  String _accountNumber;
  double _balance = 0;

  // Public member
  String ownerName;

  BankAccount(this.ownerName, this._accountNumber);

  // Public method
  void deposit(double amount) {
    if (amount > 0) {
      _balance += amount;
      print('Deposited ¥$amount, current balance: ¥$_balance');
    }
  }

  bool withdraw(double amount) {
    if (amount > 0 && amount <= _balance) {
      _balance -= amount;
      print('Withdrawn ¥$amount, current balance: ¥$_balance');
      return true;
    }
    print('Insufficient balance, withdrawal failed');
    return false;
  }

  // Private method: for internal use within the class only
  void _logTransaction(String type, double amount) {
    print('[Internal log] $type: ¥$amount');
  }
}

void main() {
  var account = BankAccount('EXAMPLE', '6222-0000-1234');

  account.deposit(1000);
  account.withdraw(300);
  account.withdraw(800);  // Insufficient balance

  // External code cannot access private members
  // print(account._balance); // Error: _balance is private
}
存入 ¥1000,当前余额: ¥1000
取出 ¥300,当前余额: ¥700
余额不足,取款失败

Getter and Setter

Getters and setters are special methods for reading and writing member variables.

They allow you to execute extra logic when accessing properties while keeping the concise syntax of property access.

Example

class Circle {
  double _radius;  // Private variable storing the radius

  Circle(this._radius);

  // Getter: returns diameter (computed property)
  double get diameter => _radius * 2;

  // Getter: returns area
  double get area => 3.14159 * _radius * _radius;

  // Getter and Setter work together: expose the radius property externally
  double get radius => _radius;

  set radius(double value) {
    // Validation logic can be added in the Setter
    if (value <= 0) {
      throw ArgumentError('Radius must be greater than 0');
    }
    _radius = value;
  }

  // Read-only Getter: no corresponding Setter
  String get info => 'Circle(radius: $_radius)';
}

void main() {
  var circle = Circle(5);

  // Using Getter (like accessing a normal property)
  print('EXAMPLE Circle: ${circle.info}');
  print('Diameter: ${circle.diameter}');
  print('Area: ${circle.area.toStringAsFixed(2)}');

  // Using Setter
  circle.radius = 10;
  print('Modified diameter: ${circle.diameter}');

  // Setting an invalid value will throw an exception
  // circle.radius = -1; // throws ArgumentError
}
EXAMPLE 圆形: 圆(半径: 5.0)
直径: 10.0
面积: 78.54
修改后直径: 20.0

The advantage of getters and setters is that the caller does not need to know whether `radius` is a simple field or a computed property. If validation logic needs to be added to `radius` in the future, you can simply change the field to a getter/setter, and the calling code does not need to be modified. This is the benefit of encapsulation.


Static members

Static members belong to the class itself, not to any instance of the class.

All instances share the same static variable, and static methods can be called without creating an object.

Example

class Counter {
  // Static variable: shared by all instances
  static int totalCount = 0;

  // Instance variable: independent for each instance
  String label;

  Counter(this.label) {
    // Each time an instance is created, the static counter increments by 1
    totalCount++;
  }

  // Static method: called via the class name
  static void printTotal() {
    print('EXAMPLE total instance count: $totalCount');
  }

  // Static methods are often used for utility functions
  static bool isValidLabel(String label) {
    return label.isNotEmpty && label.length <= 20;
  }
}

void main() {
  // Static method: called directly via the class name, no instance creation required
  print('Is label valid: ${Counter.isValidLabel('test')}');

  var c1 = Counter('Counter A');
  Counter.printTotal();  // 1

  var c2 = Counter('Counter B');
  var c3 = Counter('Counter C');
  Counter.printTotal();  // 3

  // Static variable: accessed via the class name
  print('Final count: ${Counter.totalCount}');
}
标签是否有效: true
EXAMPLE 总实例数: 1
EXAMPLE 总实例数: 3
最终计数: 3

Static methods cannot access instance members (because there is no `this`). Static methods are suitable for utility functions and factory logic, but do not overuse them—static methods cannot be overridden by subclasses, and overuse will reduce the testability and flexibility of the code.

other extensions