An immutable class never changes its fields, so updates produce a new instance. copyWith takes nullable named parameters and uses ?? to fall back to this's value when the caller omits an argument. The original is unchanged.

Program

Play the program to build a Todo, copy it with done: true, and compare the two labels.

copy_with.dart
class Todo {
  final String title;
  final bool done;
  const Todo(this.title, {this.done = false});
  Todo copyWith({String? title, bool? done}) => Todo(title ?? this.title, done: done ?? this.done);
  String label() => '[${done ? 'x' : ' '}] $title';
}

void main() {
  final original = Todo('Write notes');
  final completed = original.copyWith(done: true);
  print('${original.label()} -> ${completed.label()}');
}
copyWith `copyWith({String? title, bool? done})` builds a fresh `Todo`, picking new field values where the caller supplied them.
null-coalescing `title ?? this.title` keeps the original value when the caller omits the argument; `done ?? this.done` does the same for `done`.
immutable update `original` still has `done: false` after the call; `completed` is a separate instance with `done: true`.