Lesson 10 / 25

Classes, Constructors and Initialiser Lists

Write classes with generative, named, const and factory constructors.

Constructors in many shapes

Dart classes have fields, methods, getters and setters. Constructors come in several forms. Generative constructors with initialising formal parameters assign fields concisely: Product(this.title, this.price);. Named constructors create alternatives: Product.free(this.title) : price = 0;. An initialiser list after a colon sets final fields and runs asserts before the body. const constructors (all fields final) allow compile-time constant instances, essential for efficient Flutter widgets. factory constructors can return cached instances, subclasses or instances built from data, such as factory Product.fromJson(Map<String, dynamic> json). Fields marked final must be set by the time the constructor finishes, and late final fields can be set once later. Private members start with an underscore (_secret) and are private to the library (the file and its parts), not the class. Getters (double get total => ...) compute values, and Dart provides == and hashCode only by identity unless you override them, which packages such as equatable or code generators can help with.

Ways to create an object

Generative, named, const and factory constructors produce instances in different ways.

Four entry arrows from different shaped gates converging on one object shape.
Figure 4.1 — Constructor kinds in Dart.

A model class with several constructors

const, named and factory constructors with an initialiser list.

class Product {
  const Product(this.sku, this.title, this.pricePaise)
      : assert(pricePaise >= 0);

  const Product.free(this.sku, this.title) : pricePaise = 0;     // named constructor

  factory Product.fromJson(Map<String, dynamic> json) {          // factory constructor
    return Product(
      json['sku'] as String,
      json['title'] as String,
      (json['price_paise'] as num).toInt(),
    );
  }

  final String sku;
  final String title;
  final int pricePaise;

  String get displayPrice => 'Rs ${(pricePaise / 100).toStringAsFixed(2)}';   // getter

  Map<String, dynamic> toJson() => {'sku': sku, 'title': title, 'price_paise': pricePaise};

  @override
  bool operator ==(Object other) =>
      other is Product && other.sku == sku && other.pricePaise == pricePaise;

  @override
  int get hashCode => Object.hash(sku, pricePaise);
}

void main() {
  const pen = Product('PEN', 'Blue pen', 4950);
  final sample = Product.free('SAMPLE', 'Sample sachet');
  final fromApi = Product.fromJson({'sku': 'INK', 'title': 'Ink', 'price_paise': 19900});
  print(pen.displayPrice);         // Rs 49.50
}

Override == and hashCode together

If two objects are equal by ==, they must have the same hashCode, or sets and maps behave incorrectly. Object.hash(...) makes a correct implementation easy.

Quick check: What can a factory constructor do that a generative constructor cannot?

  • Take parameters
  • Initialise final fields
  • Return an existing cached instance or a subtype instead of always creating a new instance of the class
  • Be called with new
Answer

Return an existing cached instance or a subtype instead of always creating a new instance of the class — Factory constructors decide what to return, including cached objects or subclasses.