An immutable data object pairs final fields with a const constructor. Each instance is frozen after construction, and the constructor can also be used in const contexts. A small method that reads the fields keeps presentation logic with the data.

Program

Play the program to build two Todo values and print their labels.

immutable_pattern.dart
Replay: real traced execution (multi-file project)
class Todo {
  final String title;
  final bool done;
  const Todo(this.title, {this.done = false});
  String label() => '[${done ? 'x' : ' '}] $title';
}

void main() {
  final a = Todo('Write notes', done: true);
  final b = Todo('Buy milk');
  print('${a.label()} | ${b.label()}');
}
  1. a ← constructing Todo('Write notes', done: true)

    8void main() {9  final a = Todo('Write notes', done: true);10  final b = Todo('Buy milk');
    values this stepconstructing Todo('Write notes', done: true)a
  2. fields ← title=Write notes, done=true

    3final bool done;4const Todo(this.title, {this.done = false});5String label() => '[${done ? 'x' : ' '}] $title';
    values this steptitle=Write notes, done=truefields
  3. a ← Todo(title: Write notes, done: true)

    8void main() {9  final a = Todo('Write notes', done: true);10  final b = Todo('Buy milk');
    values this stepTodo(title: Write notes, done: true)a
  4. b ← constructing Todo('Buy milk')

    9final a = Todo('Write notes', done: true);10final b = Todo('Buy milk');11print('${a.label()} | ${b.label()}');
    values this stepconstructing Todo('Buy milk')b
  5. fields ← title=Buy milk, done=false

    3final bool done;4const Todo(this.title, {this.done = false});5String label() => '[${done ? 'x' : ' '}] $title';
    values this steptitle=Buy milk, done=falsefields
  6. b ← Todo(title: Buy milk, done: false)

    9final a = Todo('Write notes', done: true);10final b = Todo('Buy milk');11print('${a.label()} | ${b.label()}');
    values this stepTodo(title: Buy milk, done: false)b
  7. return ← [x] Write notes

    4  const Todo(this.title, {this.done = false});5  String label() => '[${done ? 'x' : ' '}] $title';6}
    values this step[x] Write notesreturntruedoneWrite notestitle
  8. return ← [ ] Buy milk

    4  const Todo(this.title, {this.done = false});5  String label() => '[${done ? 'x' : ' '}] $title';6}
    values this step[ ] Buy milkreturnfalsedoneBuy milktitle
  9. print('${a.label()} | ${b.label()}');

    10  final b = Todo('Buy milk');11  print('${a.label()} | ${b.label()}');12}
    output[x] Write notes | [ ] Buy milk
    values this step[x] Write notesa.label()[ ] Buy milkb.label()
final fields `final String title; final bool done;` freeze the values after construction.
const constructor `const Todo(this.title, {this.done = false});` supports ordinary construction here and also allows compile-time constants when called with `const`.
named default `{this.done = false}` gives the named parameter a default; `Todo('Buy milk')` omits it.