| |

Dart 22 🎯 Getters, Setters, and Computed Properties

A field holds a value. A getter computes one. From the outside, they can look identical—user.name reads the same whether name is a stored field or a computed value. This indistinguishability is deliberate. Dart’s getter and setter syntax lets you start with a simple field and later replace it with a computed property without changing the call sites. The interface stays the same; the implementation evolves.

This chapter covers getters, setters, and computed properties in Dart. You will learn the syntax for instance and static getters, the rules that govern setters, how getters differ from methods, and how to use computed properties to derive values from other state. You will also see the late modifier and its interaction with getters, the final field pattern with getter syntax, and the conventions that make computed properties readable.

By the end, you will understand when to use a getter versus a method, when to expose a field directly, and how to write computed properties that stay fast and predictable.

Key point: A getter is a method with no parameters that is called with field syntax. A setter is a method with one parameter that is assigned with =. Dart generates them automatically for public fields, but you can declare them explicitly to compute, validate, or transform values without changing how callers interact with the class.


Why getters and setters exist

The encapsulation problem. Exposing a field directly gives callers read and write access with no control. You cannot validate an assigned value, compute a derived value on read, or change the internal representation later without breaking the public API. Getters and setters let you keep the simple field syntax while adding logic behind it.

The interface stability problem. Suppose a class exposes String name as a public field. Later, you decide that name should be computed from firstName and lastName. If name is a field, you must change every call site. If name is a getter, the call sites remain user.name, and the change is invisible to them. Dart’s uniform access principle—fields and getters look the same—makes this refactoring safe.

The computed property problem. Some values are not stored but derived. A rectangle’s area is width * height. A user’s display name might be '$firstName $lastName'. A list’s isEmpty is length == 0. Storing these values duplicates state and risks inconsistency. Computed properties derive them on demand from the source of truth.

The validation problem. A setter can reject invalid values, trim whitespace, or transform input before storing it. A public field cannot. set age(int value) { if (value < 0) throw ArgumentError(...); _age = value; } is a setter that enforces an invariant.

The lazy initialization problem. Some values are expensive to compute and rarely needed. A getter can compute them on first access and cache the result. This combines the simplicity of a field with the laziness of a method.


a. Instance getters and setters

A getter is declared with the get keyword and no parameter list. It has a return type and a body that returns a value.

class Rectangle {
  double width;
  double height;

  Rectangle(this.width, this.height);

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

The area and perimeter getters are accessed as if they were fields: rect.area, rect.perimeter. No parentheses. The expression body => is common for simple getters.

A setter is declared with the set keyword and exactly one parameter. It has no return type.

class Temperature {
  double _celsius = 0;

  double get celsius => _celsius;
  set celsius(double value) {
    if (value < -273.15) {
      throw ArgumentError('Below absolute zero');
    }
    _celsius = value;
  }

  double get fahrenheit => _celsius * 9 / 5 + 32;
  set fahrenheit(double value) {
    _celsius = (value - 32) * 5 / 9;
  }
}

The celsius setter validates input. The fahrenheit getter and setter convert between units. Callers use temp.fahrenheit = 212 and temp.celsius, unaware of the conversion.

A getter and setter can share a name. This is common when a property is both readable and writable but requires logic on one or both sides. If only a getter is defined, the property is read-only from outside the class. If only a setter is defined, it is write-only, which is rare but legal.

class User {
  String _name = '';

  String get name => _name;
  set name(String value) => _name = value.trim();
}

The setter trims whitespace. The getter returns the stored value. The property is read-write, but writes are normalized.


b. Static getters and the late modifier

Getters can be static. A static getter is accessed on the class itself, not on an instance.

class MathUtils {
  static double get pi => 3.141592653589793;
  static int get maxSafeInteger => 9007199254740991;
}

Usage: MathUtils.pi, MathUtils.maxSafeInteger. Static getters are useful for constants that require computation or for exposing values from a private static field.

The late modifier interacts with getters in a specific way. A late final field is initialized on first access. A late getter is not a thing—the modifier applies to fields, not getters. But a field declared with late and a getter that reads it can produce lazy initialization.

class ExpensiveData {
  late final List<int> _data = _compute();
  List<int> get data => _data;

  List<int> _compute() {
    // expensive computation
    return List.generate(1000000, (i) => i);
  }
}

The _data field is initialized the first time it is read. The public data getter exposes it. Subsequent reads return the cached list.

A getter itself can be lazy without late by caching the result in a private field:

class LazyValue {
  int? _cached;

  int get value {
    return _cached ??= _compute();
  }

  int _compute() => 42;
}

The ??= operator assigns _compute() to _cached only if _cached is null, then returns it. This is the manual form of lazy initialization, useful when the computation depends on mutable state.


c. Getters versus methods and the uniform access principle

Dart’s uniform access principle says that a field access and a getter call look identical. obj.value works whether value is a field or a getter. This means you can change a field to a getter without breaking callers.

This principle has a consequence: getters should behave like field accesses. They should be fast, side-effect-free, and idempotent. A getter that performs I/O, mutates state, or takes a long time violates the expectation set by the syntax.

// BAD: getter with side effects
int get nextId => _counter++;

// BAD: getter that does expensive I/O
String get config => File('config.txt').readAsStringSync();

// GOOD: getter that computes from existing state
bool get isEmpty => _items.isEmpty;

// GOOD: getter that returns a cached value
List<int> get data => _cachedData ??= _compute();

The rule of thumb: if the operation is expensive, has side effects, or depends on external state, use a method instead of a getter. A method’s call syntax—obj.compute()—signals that work is being done. A getter’s field syntax—obj.value—signals that a value is being read.

There is no performance difference between a getter and a method in Dart. Both compile to method calls. The choice is about clarity, not speed.

Getters can be overridden in subclasses, just like methods. This makes them part of the class’s interface. An abstract class can declare an abstract getter:

abstract class Shape {
  double get area;
}

class Circle extends Shape {
  final double radius;
  Circle(this.radius);

  @override
  double get area => 3.14159 * radius * radius;
}

The area getter is abstract in Shape and implemented in Circle. Any subclass must provide it.


Complete Example Session

// ============================================
// PART 1: BASIC COMPUTED GETTER
// ============================================
class Rectangle {
  double width;
  double height;

  Rectangle(this.width, this.height);

  double get area => width * height;
}
// ============================================
// PART 2: GETTER WITH VALIDATION SETTER
// ============================================
class Temperature {
  double _celsius = 0;

  double get celsius => _celsius;
  set celsius(double value) {
    if (value < -273.15) throw ArgumentError('Below absolute zero');
    _celsius = value;
  }
}
// ============================================
// PART 3: GETTER AND SETTER WITH CONVERSION
// ============================================
class Temperature {
  double _celsius = 0;

  double get fahrenheit => _celsius * 9 / 5 + 32;
  set fahrenheit(double value) => _celsius = (value - 32) * 5 / 9;

  double get celsius => _celsius;
  set celsius(double value) => _celsius = value;
}
// ============================================
// PART 4: READ-ONLY COMPUTED PROPERTY
// ============================================
class User {
  final String firstName;
  final String lastName;

  User(this.firstName, this.lastName);

  String get fullName => '$firstName $lastName';
  String get initials => '${firstName[0]}${lastName[0]}';
}
// ============================================
// PART 5: GETTER THAT TRIMS INPUT
// ============================================
class Account {
  String _email = '';

  String get email => _email;
  set email(String value) => _email = value.trim().toLowerCase();
}
// ============================================
// PART 6: STATIC GETTER
// ============================================
class MathConstants {
  static double get pi => 3.141592653589793;
  static double get e => 2.718281828459045;
}
// ============================================
// PART 7: LATE FINAL FIELD WITH GETTER
// ============================================
class DataStore {
  late final List<int> _data = _load();
  List<int> get data => _data;

  List<int> _load() => List.generate(100, (i) => i);
}
// ============================================
// PART 8: LAZY COMPUTATION WITH CACHING
// ============================================
class LazySquare {
  final int input;
  int? _cached;

  LazySquare(this.input);

  int get square => _cached ??= input * input;
}
// ============================================
// PART 9: ABSTRACT GETTER
// ============================================
abstract class Shape {
  double get area;
  double get perimeter;
}

class Circle extends Shape {
  final double radius;
  Circle(this.radius);

  @override
  double get area => 3.14159 * radius * radius;

  @override
  double get perimeter => 2 * 3.14159 * radius;
}
// ============================================
// PART 10: GETTER VERSUS METHOD
// ============================================
class ListWrapper<T> {
  final List<T> _items;
  ListWrapper(this._items);

  bool get isEmpty => _items.isEmpty;
  int get length => _items.length;
  T elementAt(int index) => _items[index];  // method: has parameter
}

The ten parts covered basic computed getters, validation setters, getters and setters with conversion, read-only computed properties, input normalization, static getters, late final fields, lazy caching with ??=, abstract getters, and the distinction between getters and methods.


Quick Reference

Getter and Setter Syntax

DeclarationSyntax
GetterType get name => expression;
Getter with blockType get name { return value; }
Setterset name(Type value) { ... }
Static getterstatic Type get name => ...
Abstract getterType get name;
Read-onlyGetter only
Write-onlySetter only
Read-writeBoth with same name

Getter vs Method

AspectGetterMethod
Call syntaxobj.valueobj.value()
ParametersNoneAny
Side effectsShould have noneMay have
Expensive workShould be cachedAcceptable
InterfacePart of propertyPart of API
OverrideYesYes

Common Patterns

PatternPurpose
Type get name => exprSimple computed value
set name(Type v) => _field = vStore with transformation
late final + getterLazy initialization
_cached ??= _compute()Manual lazy caching
Abstract getterRequired in subclass

Naming Conventions

MemberConvention
GetterNoun or adjective (area, isEmpty)
SetterSame as getter
MethodVerb (compute, update)
Boolean getteris*, has*, can*
Private field_name

Best Practices

✅ Do This:

// Use getters for computed values
double get area => width * height;                            // ✅

// Use setters for validation
set age(int value) {
  if (value < 0) throw ArgumentError();
  _age = value;
}                                                              // ✅

// Use getters for derived read-only values
String get fullName => '$firstName $lastName';                 // ✅

// Use late final for lazy initialization
late final List<int> _data = _load();                          // ✅

// Use ??= for manual caching
int get value => _cached ??= _compute();                       // ✅

// Use abstract getters for interfaces
abstract class Shape { double get area; }                      // ✅

// Use methods when work has side effects
void refresh() { ... }                                         // ✅

❌ Don’t Do This:

// Don't put side effects in getters
int get nextId => _counter++;                                  // ❌

// Don't do expensive I/O in getters
String get config => File('c.txt').readAsStringSync();         // ❌

// Don't use getters for operations with parameters
double getArea(double scale) => area * scale;                  // ❌ (use method)

// Don't expose mutable internal state directly
List<int> get items => _items;                                 // ⚠️ (returns reference)
// Use List.unmodifiable(_items) instead

// Don't create setters without getters unless intentional
set value(int v) => _v = v;                                    // ⚠️ write-only

// Don't use getters for expensive uncached computations
List<int> get sorted => _items.toList()..sort();               // ⚠️ recomputes

Common Pitfalls

PitfallWhy It HappensFix
Getter recomputes every accessNo cachingUse late final or ??=
Setter doesn’t update related stateMissing side effectsUpdate derived fields in setter
Getter with side effectsMisuse of uniform accessConvert to method
Abstract getter not implementedSubclass forgetsImplement or make class abstract
Mutable list exposedReturning internal referenceReturn List.unmodifiable
Setter and getter names differTypoUse same name
Getter throws unexpectedlyComputation errorValidate or use method

Real-World Examples

1. Full Name from Parts

String get fullName => '$firstName $lastName';

2. Formatted Currency

String get formatted => '\$${amount.toStringAsFixed(2)}';

3. Boolean State

bool get isEmpty => _items.isEmpty;
bool get isNotEmpty => _items.isNotEmpty;

4. Validation Setter

set email(String value) {
  if (!value.contains('@')) throw ArgumentError();
  _email = value;
}

5. Unit Conversion

double get fahrenheit => _celsius * 9 / 5 + 32;
set fahrenheit(double v) => _celsius = (v - 32) * 5 / 9;

6. Lazy Loaded Configuration

late final Map<String, String> _config = _loadConfig();

7. Cached Expensive Computation

BigInt? _factorialCache;
BigInt get factorial => _factorialCache ??= _computeFactorial();

8. Abstract Interface

abstract class Repository<T> {
  List<T> get all;
  T? findById(int id);
}

9. Immutable Collection Getter

List<String> get tags => List.unmodifiable(_tags);

10. Static Constant Getter

static DateTime get epoch => DateTime.utc(1970, 1, 1);

Visual

Getter and Setter Flow

┌─────────────────────────────────────────────────────────────┐
│  GETTER: READ ACCESS                                        │
│                                                             │
│  Caller: obj.area                                           │
│    │                                                        │
│    ▼                                                        │
│  Getter body executes                                       │
│    │                                                        │
│    ▼                                                        │
│  Returns computed value                                     │
│                                                             │
│  No parentheses. Looks like field access.                   │
│                                                             │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  SETTER: WRITE ACCESS                                       │
│                                                             │
│  Caller: obj.age = 25                                       │
│    │                                                        │
│    ▼                                                        │
│  Setter body executes with 25                               │
│    │                                                        │
│    ▼                                                        │
│  Validates, transforms, stores                              │
│                                                             │
│  Uses = syntax. Looks like field assignment.                │
│                                                             │
└─────────────────────────────────────────────────────────────┘

Uniform Access Principle

┌─────────────────────────────────────────────────────────────┐
│  FIELD VERSION                                              │
│                                                             │
│  class User {                                               │
│    String name;                                             │
│  }                                                          │
│                                                             │
│  Usage: user.name  →  "Alice"                               │
│                                                             │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  GETTER VERSION (same call site)                            │
│                                                             │
│  class User {                                               │
│    String _first = 'Alice';                                 │
│    String _last = 'Smith';                                  │
│    String get name => '$_first $_last';                     │
│  }                                                          │
│                                                             │
│  Usage: user.name  →  "Alice Smith"                         │
│                                                             │
│  Call site unchanged. Implementation evolved.               │
│                                                             │
└─────────────────────────────────────────────────────────────┘

Lazy Initialization Flow

┌─────────────────────────────────────────────────────────────┐
│  late final FIELD                                           │
│                                                             │
│  class Data {                                               │
│    late final List<int> _data = _load();                    │
│  }                                                          │
│                                                             │
│  First access to _data:                                     │
│    │                                                        │
│    ▼                                                        │
│  _load() executes                                           │
│    │                                                        │
│    ▼                                                        │
│  Result stored in _data                                     │
│                                                             │
│  Subsequent accesses:                                       │
│    │                                                        │
│    ▼                                                        │
│  Cached value returned immediately                          │
│                                                             │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  ??= CACHING                                                │
│                                                             │
│  int get value => _cached ??= _compute();                   │
│                                                             │
│  If _cached is null: compute and assign                     │
│  If _cached is not null: return it                          │
│                                                             │
└─────────────────────────────────────────────────────────────┘

Getter vs Method Decision

┌─────────────────────────────────────────────────────────────┐
│  CHOOSING BETWEEN GETTER AND METHOD                         │
│                                                             │
│  Does the operation take parameters?                        │
│    │                                                        │
│    ├── YES ──▶ Method                                       │
│    │                                                        │
│    └── NO                                                   │
│          │                                                  │
│          ├── Does it have side effects?                     │
│          │     ├── YES ──▶ Method                           │
│          │     └── NO                                       │
│          │                                                  │
│          ├── Is it expensive without caching?               │
│          │     ├── YES ──▶ Method                           │
│          │     └── NO ──▶ Getter                            │
│          │                                                  │
│          └── Is it a derived value?                         │
│                ├── YES ──▶ Getter                           │
│                └── NO ──▶ Field or method                   │
│                                                             │
└─────────────────────────────────────────────────────────────┘

Summary

ItemValue
Getter syntaxType get name => expression;
Setter syntaxset name(Type value) { ... }
AccessField syntax (obj.name)
AssignmentAssignment syntax (obj.name = value)
Static getterstatic Type get name => ...
Abstract getterType get name;
Read-onlyGetter without setter
Write-onlySetter without getter
Lazy initlate final field
Manual cache_cached ??= _compute()
Side effectsShould be in methods, not getters
OverrideGetters and setters are overridable

Key takeaways:

  • Getters and setters use field syntax but are methods underneath. A getter is called with obj.name; a setter with obj.name = value. No parentheses, no explicit method call.
  • The uniform access principle makes fields and getters interchangeable. You can start with a public field and later replace it with a getter without changing any call sites. This is Dart’s primary tool for evolving an API without breaking it.
  • Getters should be fast and side-effect-free. A getter that performs I/O, mutates state, or takes significant time violates the expectation set by field syntax. Use a method instead.
  • Setters can validate and transform. A setter receives the assigned value and can reject it, trim it, or convert it before storing. This keeps invariants enforced at the boundary.
  • late final fields provide lazy initialization. The field is computed on first access and cached thereafter. This combines the simplicity of a field with the laziness of a method.
  • Manual caching uses ??=. _cached ??= _compute() assigns the result only if the cache is null. This is the pattern for lazy values that depend on mutable state.
  • Abstract getters define interface contracts. An abstract class can declare double get area; and require subclasses to implement it. The getter is part of the class’s public API.
  • Getters and setters can be overridden. They participate in inheritance like methods. A subclass can provide a different implementation of a computed property.

Remember: Getters and setters are Dart’s mechanism for computed properties and controlled access. They let you keep the simple field syntax while adding logic behind it. The uniform access principle means callers never need to know whether they are reading a field or invoking a computation. Use getters for derived values, setters for validated writes, and late final or ??= for lazy initialization. Keep getters fast and side-effect-free—if the operation is expensive or has side effects, a method is the right choice. The syntax is minimal, but the design decision—field, getter, or method—shapes how the class behaves and how it evolves.



Stop using slow, ad-bloated tool sites! 🤮

🔎 Search “KandZ Tools” on Google to use many professional utilities for free.

KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)

⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free

🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!