Skip to main content
A final field in Dart is a class-level variable declared with the final keyword, enforcing strict single-assignment semantics. Once a final field is initialized, its memory reference is locked and cannot be reassigned. At compile time, Dart implicitly generates a getter for a final field but omits the setter, rendering the field read-only after instantiation.

Initialization Mechanics

Dart requires that all final fields (both nullable and non-nullable) be explicitly initialized before the constructor body executes. A nullable final field (e.g., final int? x;) does not implicitly default to null to satisfy the final assignment contract; it will throw a compile-time error if left uninitialized. Initialization must be achieved through one of three primary mechanisms: 1. Inline Initialization The field is assigned a value directly at the point of declaration.
class Configuration {
  final String environment = 'production';
}
2. Initializing Formals The field is initialized via the constructor signature using this.fieldName. This is syntactic sugar that binds the passed argument directly to the field before the constructor body runs.
class Point {
  final int x;
  final int y;

  Point(this.x, this.y);
}
3. Initializer Lists The field is assigned a value in the constructor’s initializer list, which executes after arguments are evaluated but before the constructor body. This is required when the initialization requires computation or when mapping constructor parameters to fields with different names. Because an initializing formal (this.radius) cannot be accessed within the initializer list, a standard parameter must be used to perform computations.
class Circle {
  final double radius;
  final double area;

  Circle(double radius) : this.radius = radius, area = 3.14159 * radius * radius;
}

Shallow Immutability

The final keyword enforces shallow immutability. It protects the variable’s reference, not the underlying object’s state. If a final field holds a reference to a mutable object (such as a List, Map, or a custom class instance), the internal state of that object can still be modified.
class DataContainer {
  final List<String> items = [];

  void mutate() {
    // Valid: Mutating the state of the referenced object
    items.add('new item'); 
    
    // Invalid: Reassigning the reference causes a compile-time error
    // items = ['another', 'list']; 
  }
}

late final Fields

By combining the late and final modifiers, the initialization of a final field can be deferred until runtime, bypassing the strict requirement to initialize it during construction. A late final field retains single-assignment semantics; attempting to assign a value to it a second time will throw a late initialization error (a subclass of Error) at runtime.
class Database {
  late final String connectionString;

  void initialize(String url) {
    // Valid on the first call. 
    // Throws a runtime error if called again.
    connectionString = url; 
  }
}
Tired of Poor Dart Skills? Fix That With Deep Grasping!Learn More