ZetCode

Getters and Setters in Dart

last modified October 18, 2026

In this article we explore getters and setters in Dart, the accessors that control how a class exposes and stores its data.

Getters and Setters Overview

A getter is a method that is accessed like a field, and a setter is a method that is assigned to like a field. Together they are called accessors. Dart's syntax makes them invisible to callers: obj.value and obj.value = x look like plain field access even when they run custom code.

Accessors matter for encapsulation. Instead of exposing a raw field, a class can hide it behind a private field and control every read and write. Getters can compute values on the fly or cache them; setters can validate, normalise, or translate input. Because the accessor syntax matches field syntax, you can start with a public field and later replace it with an accessor without breaking callers.

Instance fields already provide implicit accessors. A mutable field exposes a getter and setter; an ordinary final field exposes only a getter. There is no need to write accessors that merely forward to a field without adding behavior. Privacy is library-scoped in Dart: an underscore hides a name from other libraries, not from other code in the same library.

Form Syntax Used as
Getter Type get name => ... obj.name
Setter set name(Type value) { ... } obj.name = value
Static getter static Type get name => ... Class.name
Static setter static set name(Type v) { ... } Class.name = v
Abstract accessor Type get name; Implemented by subclasses

Basic Getters and Setters

The smallest useful accessor pair wraps a private field. The getter exposes the value, and the setter validates before storing. A private field is a plain identifier starting with an underscore.

basic_accessors.dart
class Person {
  late String _name;

  Person(String name) {
    this.name = name;
  }

  String get name => _name;

  set name(String value) {
    if (value.trim().isEmpty) throw ArgumentError('empty name');
    _name = value.trim();
  }
}

void main() {
  var p = Person('  Alice  ');
  print(p.name);

  try {
    p.name = '   ';
  } catch (e) {
    print('rejected: ${e.runtimeType}');
  }
}

The field _name is hidden from other libraries, which use the public accessors. The constructor also calls the setter, ensuring that the initial name is validated rather than starting with an invalid empty value. The setter trims surrounding whitespace, so the stored value is normalised; that is why reading p.name prints Alice without spaces. Assigning only whitespace throws an ArgumentError, which the try block catches, demonstrating that the setter is the single choke point for validation.

$ dart run basic_accessors.dart
Alice
rejected: ArgumentError

Computed Properties

A getter need not return a stored field. It can compute its value from other state each time it is read. Such computed properties keep an object consistent because there is no duplicated field to fall out of sync. A setter can perform the inverse transformation.

computed_properties.dart
class Rectangle {
  double width, height;

  Rectangle(this.width, this.height);

  double get area => width * height;
  double get perimeter => 2 * (width + height);

  set area(double value) {
    if (!value.isFinite || value < 0) {
      throw ArgumentError('area must be finite and non-negative');
    }
    if (!width.isFinite || width <= 0) {
      throw StateError('width must be finite and positive to set area');
    }
    height = value / width;
  }

  @override
  String toString() =>
      'Rectangle($width x $height) area=${area.toStringAsFixed(1)}';
}

void main() {
  var r = Rectangle(4, 5);
  print(r.area);
  print(r.perimeter);

  r.area = 40;
  print(r);
}

area and perimeter are derived from width and height, so they always reflect the current dimensions. The area setter shows the reverse direction: assigning a target area adjusts height while keeping width fixed. Storing area in a field instead would risk it disagreeing with the side lengths. The setter rejects invalid areas and a non-positive or non-finite width, avoiding division by zero. In a fuller geometry API, dimension assignments should also be validated.

$ dart run computed_properties.dart
20.0
18.0
Rectangle(4.0 x 10.0) area=40.0

Validation and Encapsulation

A setter is a natural place to enforce invariants, and a getter can return a safe view of internal state. The class below guards its balance, records every change, and hands out an unmodifiable copy of its history so callers cannot mutate the internal list.

validation.dart
class BankAccount {
  double _balance = 0;
  final List<String> _history = [];

  double get balance => _balance;

  set balance(double value) {
    if (!value.isFinite || value < 0) {
      throw ArgumentError('balance must be finite and non-negative');
    }
    _history.add('set to $value');
    _balance = value;
  }

  void deposit(double amount) {
    if (!amount.isFinite || amount <= 0) {
      throw ArgumentError('deposit must be finite and positive');
    }
    balance = _balance + amount;
  }

  List<String> get history => List.unmodifiable(_history);
}

void main() {
  var acc = BankAccount();
  acc.balance = 100;
  acc.deposit(50);
  print(acc.balance);
  print(acc.history);

  try {
    acc.balance = -1;
  } catch (e) {
    print('rejected: ${e.runtimeType}');
  }
}

The setter rejects negative and non-finite balances and records each accepted assignment in a private audit list. The deposit method separately requires a positive, finite amount, then reuses the setter to validate and record the new balance. The history getter uses List.unmodifiable to return an unmodifiable snapshot, not a live view: later deposits do not change an already returned list. This example uses double to focus on accessors; real financial code should use an appropriate exact representation, such as integer minor units.

$ dart run validation.dart
150.0
[set to 100.0, set to 150.0]
rejected: ArgumentError

Static Getters and Setters

Accessors can also be static, in which case they belong to the class rather than an instance and are accessed through the class name. This is useful for configuration and shared state that should still be validated.

static_accessors.dart
class AppConfig {
  static String? _env;

  static String get env => _env ?? 'production';

  static set env(String value) {
    if (!['production', 'staging', 'dev'].contains(value)) {
      throw ArgumentError('invalid env: $value');
    }
    _env = value;
  }
}

void main() {
  print(AppConfig.env);
  AppConfig.env = 'dev';
  print(AppConfig.env);
}

The static getter returns a default when _env has not been set, and the static setter only accepts a known set of values. Both are accessed with AppConfig.env, exactly like an instance accessor but without an object. This keeps global configuration behind a single validated entry point.

$ dart run static_accessors.dart
production
dev

Abstract Getters and Setters

Accessors can be declared without a body in an abstract class, including an abstract interface class, forming part of the contract that concrete subtypes must fulfil. A subtype implements the getter and setter just like any other member.

abstract_accessors.dart
abstract class Shape {
  double get area;
  double get scale;
  set scale(double factor);
}

class Square implements Shape {
  final double _side;
  double _scale = 1;
  Square(this._side);

  @override
  double get area => _side * _side * _scale * _scale;

  @override
  double get scale => _scale;

  @override
  set scale(double factor) {
    if (!factor.isFinite || factor <= 0) {
      throw ArgumentError('scale must be finite and positive');
    }
    _scale = factor;
  }
}

void main() {
  var s = Square(3);
  print(s.area);

  s.scale = 2;
  print(s.area);
}

Shape declares area as an abstract getter and scale as an abstract getter and setter, but provides no implementation. Because Square implements Shape, it must supply all three accessors. The area getter uses the original side length and the current scale; the setter validates and replaces that scale. Assigning s.scale = 2 twice still means twice the original side length, rather than repeatedly doubling it. Use a method such as scaleBy for a cumulative operation.

$ dart run abstract_accessors.dart
9.0
36.0

Getters over a Collection

Getters are often used to derive summary values from a collection held by the object. The getter recomputes on each access, so it never goes stale as the collection changes.

collection_getters.dart
class Track {
  final String title;
  final int seconds;
  Track(this.title, this.seconds);
}

class Playlist {
  final List<Track> tracks = [];

  void add(Track t) => tracks.add(t);

  int get totalSeconds => tracks.fold(0, (sum, t) => sum + t.seconds);

  Duration get totalDuration => Duration(seconds: totalSeconds);

  @override
  String toString() => '$totalDuration across ${tracks.length} tracks';
}

void main() {
  var pl = Playlist()
    ..add(Track('A', 210))
    ..add(Track('B', 180))
    ..add(Track('C', 330));

  print(pl.totalSeconds);
  print(pl);
}

totalSeconds folds the track durations into a single value, and totalDuration builds on it to produce a Duration. Neither is stored, so adding or removing tracks automatically updates both. The cascade notation ..add(...) keeps the setup concise while the getters keep the class free of duplicated state. Each totalSeconds access traverses all tracks, so it takes linear time; getter syntax does not imply a constant-time operation.

$ dart run collection_getters.dart
720
0:12:00.000000 across 3 tracks

Source

Dart Language: Methods
Dart Language: Classes
Effective Dart: Avoid unnecessary accessors

Getters and setters let a class present a simple field-like surface while retaining full control over reads and writes. They support computed properties, validation, encapsulation, and abstract contracts, and because their syntax matches field access, a field can be upgraded to an accessor without changing any call site.

Author

My name is Jan Bodnar, and I am a passionate programmer with extensive programming experience. I have been writing programming articles since 2007. To date, I have authored over 1,400 articles and 8 e-books. I possess more than ten years of experience in teaching programming.

List all Dart tutorials.