11  final vs const in Dart

Both final and const in Dart are used to create immutable variables, but they have important differences that affect when and how you should use them. Let’s explore these differences and understand when to use each in class properties.

11.1 Basic Difference

11.1.1 final

A final variable can only be set once and is initialized when accessed for the first time. This means: - It’s a runtime constant - Its value is determined at runtime - It doesn’t need to be known at compile time

11.1.2 const

A const variable is a compile-time constant. This means: - Its value must be determined entirely at compile time - It cannot depend on any calculation that needs to happen at runtime - It creates deeply immutable values

11.2 Examples to Illustrate the Difference

// final example
final currentTime = DateTime.now(); // Works! Value determined at runtime
final username = getUserInput(); // Works! Value comes from a function call

// const example
const currentTime = DateTime.now(); // ERROR! Value not known at compile time
const pi = 3.14159; // Works! Value is known at compile time
const greeting = 'Hello, Dart!'; // Works! String literals are compile-time constants

11.3 Complex Examples

Let’s look at how they behave with collections and objects:

// final collections can have mutable content
final List<int> scores = [85, 92, 78];
scores.add(95); // This works! The list contents can change
scores = [100, 100]; // ERROR! Cannot reassign a final variable

// const collections are deeply immutable
const List<int> topScores = [100, 99, 98];
topScores.add(97); // ERROR! Cannot modify a const list

With custom objects:

class Point {
  final int x;
  final int y;
  
  const Point(this.x, this.y); // Constructor can be const
}

// Using const constructor
const origin = Point(0, 0); // Compile-time constant
final endpoint = Point(10, 10); // Runtime constant

// This works - different instances
final p1 = Point(1, 1);
final p2 = Point(1, 1);
print(identical(p1, p2)); // false - different objects in memory

// This creates identical instances - object canonicalization
const cp1 = Point(1, 1);
const cp2 = Point(1, 1);
print(identical(cp1, cp2)); // true - same object in memory

11.4 Usage in Class Properties

Now, let’s address which one to use for class properties:

11.4.1 Use final for class properties when:

  1. The value is calculated or determined at runtime
  2. The value comes from a constructor parameter
  3. The value is loaded from external sources (API, database, user input)
  4. The value is a complex object that might need internal modification
class Patient {
  final String id;
  final String name;
  final DateTime admissionDate;
  final List<String> symptoms; // List can still be modified internally
  
  Patient(this.id, this.name, this.admissionDate, this.symptoms);
}

11.4.2 Use const for class properties when:

  1. The value is a fixed constant known at compile time
  2. The value never changes across all instances of the class
  3. The property is static (shared across all instances)
class MedicalConstants {
  static const double normalBodyTemp = 37.0; // Celsius
  static const int maxHeartRate = 220;
  static const List<String> bloodTypes = ['A+', 'A-', 'B+', 'B-', 'AB+', 'AB-', 'O+', 'O-'];
}

11.5 Best Practices for Classes

11.5.1 Instance Properties

For instance properties (non-static), final is more commonly used because: - Instance properties often depend on constructor parameters - They frequently represent data that’s determined at runtime - They may need to be unique per instance

class RadiologyReport {
  final String patientId;
  final DateTime examDate;
  final String findings;
  final String impression;
  
  // These values come from constructor parameters - use final
  RadiologyReport(this.patientId, this.examDate, this.findings, this.impression);
}

11.5.2 Static Class Properties

For static properties that are universal constants: - Use static const for compile-time constants - Use static final for runtime constants that are still fixed for the class

class ImagingParameters {
  // Compile-time constants - use static const
  static const double standardXrayDose = 0.1; // mSv
  static const List<String> modalityTypes = ['CT', 'MRI', 'Ultrasound', 'X-ray', 'PET'];
  
  // Runtime constant that's calculated - use static final
  static final DateTime serviceStartDate = DateTime(2020, 1, 1);
  static final Map<String, double> doseByModality = _initDoseMap();
  
  static Map<String, double> _initDoseMap() {
    // Some complex initialization logic
    return {
      'CT': 7.0,
      'X-ray': 0.1,
      'PET': 25.0,
    };
  }
}

11.6 Performance and Memory Considerations

Using const where appropriate can have performance benefits: - Compile-time constants are optimized by the compiler - Identical const objects are canonicalized (share the same memory location) - const collections (lists, maps, sets) are more memory-efficient

11.7 Practical Decision Tree for Class Properties

When deciding between final and const for class properties:

  1. Is the property static (shared across all instances)?
    • Yes:
      • Is the value known at compile time? → Use static const
      • Is the value determined at runtime but fixed thereafter? → Use static final
    • No (instance property):
      • Will it be set in the constructor and never change? → Use final
      • Will it change after initialization? → Use neither (regular property)
  2. Is the property a collection (List, Map, Set)?
    • Do you need to modify the collection contents? → Use final (not const)
    • Is the collection fixed and known at compile time? → Consider const

11.8 Flutter-Specific Recommendations

In Flutter applications, the choice becomes even more important:

class MyWidget extends StatelessWidget {
  // These won't change and are passed from constructor - use final
  final String title;
  final Color backgroundColor;
  
  // This is a fixed list known at compile time - use const
  static const List<String> supportedLanguages = ['en', 'es', 'fr', 'de'];
  
  // Style constants used in this widget - use static const
  static const TextStyle headerStyle = TextStyle(fontSize: 18, fontWeight: FontWeight.bold);
  
  const MyWidget({required this.title, required this.backgroundColor});
  
  @override
  Widget build(BuildContext context) {
    // ...
  }
}

Flutter also encourages using const constructors for widgets when possible, to improve performance through widget canonicalization:

// This creates a new Container instance each build cycle
Widget buildNonConstWidget() {
  return Container(
    color: Colors.blue,
    child: Text('Hello'),
  );
}

// This reuses the same widget instance when possible
Widget buildConstWidget() {
  return const Container(
    color: Colors.blue,
    child: Text('Hello'),
  );
}

11.9 A Practical Example for Your Medical Context

Let’s tie this together with a more complete class example relevant to your background:

class RadiologyStudy {
  // Instance properties from constructor - use final
  final String patientId;
  final String accessionNumber;
  final DateTime studyDate;
  final String modality;
  final List<String> images; // Note: List contents can still be modified

  // Constants known at compile time - use static const
  static const List<String> validModalities = ['CT', 'MRI', 'XR', 'US', 'NM', 'MG'];
  static const double defaultWindowWidth = 400;
  static const double defaultWindowLevel = 40;
  
  // Runtime constants for the class - use static final
  static final DateTime systemImplementationDate = DateTime(2022, 5, 15);
  static final Map<String, String> modalityDescriptions = {
    'CT': 'Computed Tomography',
    'MRI': 'Magnetic Resonance Imaging',
    'XR': 'X-Ray',
    'US': 'Ultrasound',
    'NM': 'Nuclear Medicine',
    'MG': 'Mammography',
  };

  // Constructor
  RadiologyStudy({
    required this.patientId,
    required this.accessionNumber,
    required this.studyDate,
    required this.modality,
    required this.images,
  }) {
    // Validation could go here
    assert(validModalities.contains(modality), 'Invalid modality type');
  }
  
  // Method that demonstrates why images is final but not const
  void addImage(String imagePath) {
    images.add(imagePath); // This is allowed because only the reference is final
  }
}

11.10 Summary

  • final: Use for values determined at runtime that won’t change after initialization. Ideal for instance properties initialized by constructors.

  • const: Use for values known at compile time. Ideal for fixed constants, especially static class properties that are shared across all instances.

For your class properties: - Instance properties that come from constructors → final - Static properties with fixed values → static const - Static properties initialized at runtime → static final

Remember that both final and const prevent reassignment, but only const creates deeply immutable values. This distinction is particularly important when working with collections and complex objects.