Dart Fundamentals: The Language Behind Flutter · Classes, Modern Dart 3 Features and Async

Classes and Constructors

Define classes with fields, methods and getters, and use Dart's constructor shortcuts: this.field parameters, named constructors, initializer lists, const constructors, factory constructors and copyWith.

Create a free account to save this page, take notes and track your progress. Sign in

What you'll build

A Temperature value class and a Todo model like the ones you'd use in a Flutter app: immutable fields, named constructors, a factory that parses JSON-like maps, and copyWith for updates.

The idea

  • Fields and methods live in a class; getters compute values (double get fahrenheit => ...).
  • this.field constructor parameters assign fields directly: Todo(this.title, {this.done = false});.
  • Named constructors offer alternative ways to create objects: Temperature.fromFahrenheit(98.6).
  • Initializer lists (: celsius = (f - 32) * 5 / 9) set final fields before the body runs.
  • const constructors create compile-time constants when all fields are final.
  • factory constructors can return an existing instance or a subtype, or parse input (Todo.fromMap(json)).
  • Private members start with _ (private to the library/file).

For immutable models (common in Flutter state management), keep fields final and add a copyWith method that returns a modified copy.

The code

Value classes with named, const and factory constructors
class Temperature {
  final double celsius;
  const Temperature(this.celsius);
  Temperature.fromFahrenheit(double f) : celsius = (f - 32) * 5 / 9; // initializer list
  double get fahrenheit => celsius * 9 / 5 + 32;
  bool get isFreezing => celsius <= 0;
  @override
  String toString() => '${celsius.toStringAsFixed(1)}°C';
}

class Todo {
  final String id;
  final String title;
  final bool done;
  const Todo({required this.id, required this.title, this.done = false});

  factory Todo.fromMap(Map<String, Object?> map) {
    final title = map['title'];
    if (title is! String || title.trim().isEmpty) {
      throw FormatException('todo needs a title: $map');
    }
    return Todo(id: '${map['id']}', title: title.trim(), done: map['done'] == true);
  }

  Todo copyWith({String? title, bool? done}) =>
      Todo(id: id, title: title ?? this.title, done: done ?? this.done);

  @override
  String toString() => '[${done ? 'x' : ' '}] $title';
}

void main() {
  const freezing = Temperature(0);
  final fever = Temperature.fromFahrenheit(100.4);
  print('$freezing is ${freezing.fahrenheit}°F, freezing: ${freezing.isFreezing}');
  print('fever: $fever');

  final todo = Todo.fromMap({'id': 1, 'title': '  Buy coffee ', 'done': false});
  final finished = todo.copyWith(done: true);
  print('$todo -> $finished (original unchanged: ${todo.done == false})');

  try {
    Todo.fromMap({'id': 2, 'title': ''});
  } on FormatException catch (e) {
    print('rejected: ${e.message}');
  }

  print(identical(const Temperature(21), const Temperature(21))); // const objects are canonicalised
}

Output

0.0°C is 32.0°F, freezing: true
fever: 38.0°C
[ ] Buy coffee -> [x] Buy coffee (original unchanged: true)
rejected: todo needs a title: {id: 2, title: }
true

Run this on your computer — see “Get Started” for setup.

Common mistakes

  • Mutable public fields in models, which make state changes hard to track; prefer final fields and copyWith.
  • Forgetting @override toString(), so prints show Instance of 'Todo'.
  • Using a regular constructor to parse input. A factory can validate and throw before building.
  • Equality. Two Todos with the same fields aren't == unless you override == and hashCode (or use records or a code generator).

Key points

  • this.field parameters, named constructors and initializer lists keep constructors short.
  • const constructors for immutable compile-time values; factory for parsing and caching.
  • Getters compute derived values; _name is library-private.
  • Immutable models + copyWith are the standard Flutter style.

Further reading: Classes, Constructors.

Practice questions

What is a factory constructor in Dart, and when would you use one?

Answer

A constructor marked factory that doesn't have to create a new instance of exactly its class: it can return a cached instance, a subtype, or validate/parse input before constructing (e.g. Todo.fromMap(json)).

Why do Flutter apps often use immutable model classes with copyWith?

Answer

Immutable objects can't change behind your back, which makes state changes explicit and easy to compare. copyWith returns a modified copy, so updating state means replacing objects rather than mutating them.

Open in the app to mark lessons complete, take notes and track your progress.

Was this page helpful?