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.
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.
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.
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.
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.
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.
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.
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:
- Use final by default: Prefer final over var for variables that won't be reassigned. This makes the code's intent clearer and prevents accidental modifications.
- Combine with late for deferred initialization: Use late final for variables that need to be initialized after declaration, such as in dependency injection or when values are computed later.
- Understand collection behavior: Remember that final collections allow content modification. Use const for fully immutable collections when appropriate.
- Initialize in constructors: For class properties, initialize final instance variables in the constructor or at declaration to ensure immutability.
- Document intent with final parameters: Use final in function and constructor parameters to explicitly indicate that they won't be modified, improving code clarity.
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
List all Dart tutorials.