Skip to main content
Field overriding in Dart is the mechanism by which a subclass redeclares a member variable defined in its superclass. Because Dart fields are implicitly composed of a storage slot and accessor methods (a getter, plus a setter for non-final fields), overriding a field constitutes a polymorphic override of these accessors.

Syntax and Annotation

The @override annotation marks a field as a replacement for a member in a superclass. It enforces a compile-time check to ensure the superclass defines the member being overridden.
class SuperClass {
  int value = 10;
}

class SubClass extends SuperClass {
  @override
  int value = 20; // Overrides the implicit getter and setter of SuperClass.value
}

Polymorphic Dispatch

Unlike languages that use static binding for fields (where the field accessed is determined by the compile-time type of the reference, often resulting in shadowing), Dart uses dynamic dispatch. In Dart, field access is semantically equivalent to a method call. When a field is overridden, the subclass’s implicit accessors replace the superclass’s accessors in the object’s vtable. Consequently, accessing the field on an instance of the subclass always invokes the subclass’s implementation, even if the instance is statically typed as the superclass.
class Base {
  String text = "Base";
}

class Derived extends Base {
  @override
  String text = "Derived";
}

void main() {
  Base obj = Derived();
  // Invokes Derived.get_text() due to dynamic dispatch
  print(obj.text); // Output: "Derived"
}

Overriding Configurations

Dart treats fields and property accessors (getters/setters) interchangeably within the interface, allowing three specific overriding configurations.

1. Field Overridden by Field

A subclass declares a variable with the same name as the superclass.
  • Storage: Both the superclass and the subclass allocate independent storage slots for the field. The superclass initializer runs during instantiation, assigning a value to the superclass’s slot.
  • Access: The subclass generates new implicit accessors that read from and write to the subclass’s storage slot. The superclass’s storage slot is hidden from the subclass’s public interface but remains accessible within the subclass implementation via the super keyword.
class A {
  String text = "Base Storage";
}

class B extends A {
  @override
  String text = "Derived Storage"; // The active storage for instances of B

  void printBoth() {
    print(text);       // Accesses B.text
    print(super.text); // Accesses A.text
  }
}

2. Field Overridden by Accessors

A subclass replaces a stored variable with explicit computed properties.
  • Storage: The superclass still allocates memory for its field and executes its initializers.
  • Access: The subclass intercepts all read and write operations via its explicit methods. The superclass storage remains allocated but is bypassed by the subclass interface unless explicitly accessed via super.
class A {
  int x = 0; // Initialized to 0 during B's instantiation
}

class B extends A {
  @override
  int get x => 100; // Intercepts read access

  @override
  set x(int val) {
    print("Setting x to $val"); // Intercepts write access
  }
}

3. Accessors Overridden by Field

A subclass replaces abstract or concrete getters/setters with a stored variable.
  • Storage: The subclass allocates a storage slot.
  • Access: The compiler generates implicit accessors for the new field, satisfying the interface contract defined by the superclass’s methods.
abstract class A {
  int get id; // Abstract getter
}

class B extends A {
  @override
  int id = 1; // Concrete field implements the abstract getter
}

Mutability Constraints

The mutability of the overriding field is constrained by the contract established in the superclass.
  1. Final to Non-final (Expansion): A final field (read-only) in the superclass can be overridden by a non-final field (read-write) in the subclass. The subclass satisfies the “read” contract of the superclass and expands the interface by adding a setter.
  2. Non-final to Final (Contraction): A non-final field (read-write) in the superclass cannot be overridden by a final field. The superclass contract requires a setter, which a final field fails to provide.
class Base {
  final int readOnly = 1;
  int readWrite = 2;
}

class Derived extends Base {
  // Valid: Satisfies 'get readOnly', adds 'set readOnly'
  @override
  int readOnly = 10; 

  // Compilation Error: 'Derived.readWrite' inherits a setter requirement from 'Base', 
  // but the overriding field is final and does not provide one.
  // @override
  // final int readWrite = 20; 
}

Type Constraints and Covariance

Dart enforces strict type safety rules based on the generated accessors:
  • Getters: The return type of the overriding getter must be a subtype of (or the same as) the overridden getter.
  • Setters: The parameter type of the overriding setter must be a supertype of (or the same as) the overridden setter (contravariance).
Because a standard read-write field generates both a getter and a setter, the type of an overriding read-write field must match the superclass field’s type exactly to satisfy both constraints simultaneously.

The covariant Keyword

To override a field with a narrower subtype (e.g., overriding num with int), the covariant keyword is required. This disables the static check on the implicit setter’s parameter type, allowing the subclass to accept only the specific subtype.
class Parent {
  num number = 0;
}

class Child extends Parent {
  // Error: Implicit setter expects 'num', but 'int' narrows the type.
  // @override
  // int number = 10; 

  // Valid: 'covariant' allows narrowing the type to 'int'.
  @override
  covariant int number = 10; 
}
Master Dart with Deep Grasping Methodology!Learn More