ZetCode

Final Keyword in Dart

last modified May 25, 2025

This tutorial explores the final keyword in Dart, a modifier used to declare variables that can be set only once. Final variables are initialized at runtime and provide immutability for values that shouldn't change after assignment.

Final Keyword Overview

The final keyword in Dart is used to declare variables that can be assigned only once. Unlike const, final variables are initialized at runtime rather than compile-time. This makes them more flexible while still enforcing immutability.

Final variables must be initialized before they are used, either at declaration or in the constructor (for instance variables). Once assigned, their value cannot be changed. Final is commonly used for values that are known at runtime but shouldn't change afterward.

Feature Final Const Var
Mutability Immutable Immutable Mutable
Initialization Runtime Compile-time Runtime
Reassignment No No Yes
Use Case Runtime constants Compile-time constants Regular variables

Final is particularly useful when you need a value to remain constant after initialization, but the value isn't known until runtime. It helps prevent accidental modifications and makes code more predictable and maintainable.

Basic Final Variable

This example demonstrates the simplest use of final to declare a variable that can be set only once. We'll create a final variable and attempt to modify it.

basic_final.dart
void main() {
  // Declaring a final variable
  final String greeting = 'Hello, Dart!';

  print(greeting);

  // This would cause a compile-time error:
  // greeting = 'Goodbye'; // Error: Can't assign to the final variable 'greeting'

  // Final variables can be initialized only once
  final currentTime = DateTime.now();
  print('Current time: $currentTime');

  // This would also cause an error:
  // currentTime = DateTime.now(); // Error: Can't assign to the final variable 'currentTime'
}

The example shows a final string variable that cannot be reassigned. It also demonstrates that final variables can be initialized with runtime values like the current time, which isn't possible with const. The commented lines show attempts to reassign final variables, which would cause errors.

Final variables provide type safety and immutability while allowing values to be determined at runtime. This makes them more flexible than const while still preventing unintended modifications.

$ dart run basic_final.dart
Hello, Dart!
Current time: 2025-05-25 14:30:45.123456

Final with Collections

Final can be used with collections (List, Map, Set), making the reference immutable but allowing modification of the collection's contents. This example shows how final affects collection behavior.

final_collections.dart
void main() {
  // Final list - reference cannot change but content can
  final List<int> numbers = [1, 2, 3];

  print('Original list: $numbers');

  // Can modify the list's contents
  numbers.add(4);
  numbers[0] = 10;
  print('Modified list: $numbers');

  // Cannot reassign the list
  // numbers = [5, 6, 7]; // Error: Can't assign to the final variable 'numbers'

  // Final map example
  final Map<String, int> ages = {'Alice': 30, 'Bob': 25};
  ages['Charlie'] = 28;
  print('Ages: $ages');

  // Final set example
  final Set<String> colors = {'red', 'green'};
  colors.add('blue');
  print('Colors: $colors');
}

The example demonstrates that while the reference to a final collection cannot change, the contents of the collection can still be modified. This is different from const collections where both the reference and contents are immutable.

This behavior is useful when you want to ensure a variable always refers to the same collection instance, but still need to modify the collection's contents during program execution.

$ dart run final_collections.dart
Original list: [1, 2, 3]
Modified list: [10, 2, 3, 4]
Ages: {Alice: 30, Bob: 25, Charlie: 28}
Colors: {red, green, blue}

Final Instance Variables

Final is commonly used for instance variables in classes to create immutable properties. These can be initialized either at declaration or in the constructor.

final_instance.dart
class Person {
  // Final instance variable initialized at declaration
  final String species = 'Human';

  // Final instance variables initialized in constructor
  final String name;
  final int birthYear;

  Person(this.name, this.birthYear);

  int get age {
    final currentYear = DateTime.now().year;
    return currentYear - birthYear;
  }

  void describe() {
    print('$name is a $species born in $birthYear (age $age)');
  }
}

void main() {
  final person = Person('Alice', 1990);
  person.describe();

  // These would cause errors:
  // person.name = 'Bob'; // Error: Can't assign to the final variable 'name'
  // person.species = 'Alien'; // Error: Can't assign to the final variable 'species'

  // Final local variable in method
  final message = 'Hello, ${person.name}';
  print(message);
}

The Person class demonstrates final instance variables initialized both at declaration and through the constructor. Once set, these values cannot be changed, ensuring the object's properties remain constant after creation.

The example also shows a final local variable in a method and a getter that uses a final variable. Final variables are commonly used in classes to create immutable data models where properties shouldn't change after instantiation.

$ dart run final_instance.dart
Alice is a Human born in 1990 (age 35)
Hello, Alice

Final with Late Initialization

Dart's late modifier can be combined with final to defer initialization while still ensuring the variable is set only once. This is useful for dependency injection or lazy initialization.

late_final.dart
class DatabaseService {
  void connect() => print('Database connected');
}

class AppConfig {
  // Late final variable - must be initialized before use
  late final String apiKey;

  void initialize(String key) {
    apiKey = key;
    print('API key set to: $apiKey');

    // This would cause an error:
    // Error: Late final variable 'apiKey' is already initialized
    // apiKey = 'new-key';
  }
}

void main() {
  // Late final variable initialized when needed
  late final DatabaseService dbService;

  print('Initializing application...');
  dbService = DatabaseService();
  dbService.connect();

  final config = AppConfig();
  // Error: LateInitializationError if accessed before initialization
  print(config.apiKey);
  config.initialize('abc123xyz');
}

The example shows late final variables that are initialized after declaration but before use. The late modifier allows deferring initialization while final ensures the variable can be set only once. This pattern is useful for values that aren't available immediately but must be set before being used.

Attempting to access a late final variable before initialization throws a LateInitializationError. This provides runtime safety to ensure variables are properly initialized before use.

$ dart run late_final.dart
Initializing application...
Database connected
API key set to: abc123xyz

Final in Function Parameters

Final can be used in function parameters to prevent modification of the parameter within the function. This helps document that the parameter shouldn't be changed.

final_parameters.dart
import 'dart:math';

void processUser(final String username, final int userId) {
  // These would cause errors:
  // username = 'new_name'; // Error: Can't assign to the final variable 'username'
  // userId = 456; // Error: Can't assign to the final variable 'userId'

  print('Processing user $userId: $username');

  final localId = userId; // Can create new final variables from parameters
  final message = 'Welcome, $username';
  print(message);
}

class Point {
  final double x, y;

  Point(final this.x, final this.y);

  Point.origin()
      : x = 0,
        y = 0;

  double distanceTo(final Point other) {
    final dx = x - other.x;
    final dy = y - other.y;
    return sqrt(dx * dx + dy * dy);
  }
}

void main() {
  processUser('alice123', 123);

  final p1 = Point(3, 4);
  final p2 = Point.origin();
  print('Distance: ${p1.distanceTo(p2)}');
}

The example demonstrates final parameters in both functions and constructors. While Dart parameters are effectively final by default, explicitly using the final keyword makes the intention clearer and prevents accidental reassignment.

Final parameters are particularly useful in constructors and methods where you want to ensure parameters aren't modified before being assigned to instance variables or used in calculations. This improves code clarity and safety.

$ dart run final_parameters.dart
Processing user 123: alice123
Welcome, alice123
Distance: 5.0

Final vs Const

This example compares final and const to illustrate their differences in initialization time, usage with collections, and object creation.

final_vs_const.dart
void main() {
  // Final can be initialized at runtime
  final currentTime = DateTime.now();
  print('Current time: $currentTime');

  // Const must be initialized with compile-time constant
  const maxAttempts = 3;
  print('Max attempts: $maxAttempts');

  // This would cause an error:
  // const currentTime2 = DateTime.now(); // Error: Not a constant expression

  // Final list vs const list
  final finalList = [1, 2, 3];
  finalList.add(4); // Allowed
  print('Final list: $finalList');

  const constList = [1, 2, 3];
  // constList.add(4); // Error: Cannot add to an unmodifiable list
  print('Const list: $constList');

  // Final object vs const object
  final finalPoint = Point(1, 2);
  const constPoint = Point.origin(); // Only if Point has const constructor

  print('Final point: $finalPoint');
  print('Const point: $constPoint');
}

class Point {
  final double x, y;

  // Regular constructor
  Point(this.x, this.y);

  // Const constructor
  const Point.origin() : x = 0, y = 0;

  @override
  String toString() => 'Point($x, $y)';
}

The example highlights key differences between final and const. Final variables can be initialized with runtime values, while const requires compile-time constants. Final collections allow content modification, while const collections are completely immutable.

For objects, const requires a const constructor and all values must be compile-time constants. Final objects can use regular constructors and runtime values. Use const when values are known at compile-time and won't change.

$ dart run final_vs_const.dart
Current time: 2025-05-25 14:30:45.123456
Max attempts: 3
Final list: [1, 2, 3, 4]
Const list: [1, 2, 3]
Final point: Point(1.0, 2.0)
Const point: Point(0.0, 0.0)

Final in Control Flow

Final variables can be initialized conditionally in control flow structures, as long as they're assigned exactly once. This example shows final variables in different control flow scenarios.

final_control_flow.dart
void main() {
  final bool isLoggedIn;
  final String userRole;

  // Simulate authentication
  final authResult = authenticate();

  if (authResult['success']) {
    isLoggedIn = true;
    userRole = authResult['role'] ?? 'user';
  } else {
    isLoggedIn = false;
    userRole = 'guest';
  }

  print('Logged in: $isLoggedIn');
  print('Role: $userRole');

  // Final in try-catch
  late final String apiResponse;
  try {
    apiResponse = fetchData();
  } catch (e) {
    apiResponse = 'Error: $e';
  }

  print('API response: $apiResponse');
}

Map<String, dynamic> authenticate() {
  // Simulate authentication result
  return {'success': true, 'role': 'admin'};
}

String fetchData() {
  // Simulate API call
  return 'Data fetched successfully';
}

The example demonstrates how final variables can be initialized in different control flow constructs, such as if-else and try-catch blocks. The Dart compiler ensures that final variables are assigned exactly once, providing safety while allowing flexibility in initialization logic.

The authenticate function simulates a login process, returning a map with a success flag and user role. The fetchData function simulates an API call, returning a success message or throwing an error if it fails.

$ dart run final_control_flow.dart
Logged in: true
Role: admin
API response: Data fetched successfully

Best Practices for Using Final

Using the final keyword effectively can improve code readability, safety, and maintainability. Here are some best practices for working with final variables in Dart:

Source

Dart Language Tour: Final and Const
Dart Object Class

The final keyword in Dart provides a powerful way to enforce immutability for variables initialized at runtime. By understanding its differences from const and applying it in variables, collections, instance variables, parameters, and control flow, developers can write safer and more predictable code. Use final to signal immutability and improve the robustness of your Dart applications.

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.