Dart Programming Fundamentals

Enums & Constants

35 min Lesson 12 of 13

Why Constants and Enums Matter

In every application, there are values that should never change -- a tax rate, the maximum number of login attempts, or a set of predefined statuses like "active", "inactive", and "banned". Dart gives you powerful tools to represent these fixed values: constants (const and final) and enums. Using them correctly makes your code safer, more readable, and easier to maintain.

The const Keyword -- Compile-Time Constants

A const variable is a compile-time constant. Its value must be known and fixed before the program even runs. The Dart compiler replaces every reference to a const with its actual value during compilation.

Basic const Usage

void main() {
  const double pi = 3.14159;
  const int maxAttempts = 5;
  const String appName = 'MyDartApp';

  print(pi);          // 3.14159
  print(maxAttempts);  // 5
  print(appName);      // MyDartApp

  // ERROR: Cannot change a const variable
  // pi = 3.14;  // Compile-time error!
}
Note: const values must be determinable at compile time. You cannot assign the result of a function call, user input, or anything that depends on runtime to a const variable.

What Can Be const?

void main() {
  // These are valid const values:
  const a = 42;                    // Literal number
  const b = 'hello';               // Literal string
  const c = true;                  // Literal bool
  const d = 3 * 7;                 // Compile-time arithmetic
  const e = 'Hello, $b world';    // Compile-time string interpolation (if b is const)

  // These are NOT valid as const:
  // const f = DateTime.now();     // ERROR: Runtime value
  // const g = int.parse('42');   // ERROR: Function call at runtime
}

The final Keyword -- Runtime Constants

A final variable can only be set once, but its value can be determined at runtime. Once assigned, it cannot be changed. Think of final as "assign once, read forever".

Basic final Usage

void main() {
  final DateTime now = DateTime.now();  // Set at runtime
  final String greeting = 'Hello at ${now.hour}:${now.minute}';

  print(now);       // 2026-03-12 14:30:00.000 (example)
  print(greeting);  // Hello at 14:30

  // ERROR: Cannot reassign a final variable
  // now = DateTime.now();  // Compile-time error!
}

final vs var -- The Difference

void main() {
  var name = 'Alice';
  name = 'Bob';    // OK: var can be reassigned

  final city = 'Riyadh';
  // city = 'Dubai';  // ERROR: final cannot be reassigned

  // final still allows mutation of the object itself
  final List<int> numbers = [1, 2, 3];
  numbers.add(4);       // OK: Modifying the list contents
  print(numbers);       // [1, 2, 3, 4]
  // numbers = [5, 6];  // ERROR: Cannot reassign the variable
}
Warning: A final list or map is not immutable -- you can still add, remove, or change elements. The variable just cannot point to a different list. If you want a truly immutable collection, use const.

const vs final -- Key Differences

Understanding when to use const versus final is one of the most important decisions in Dart programming.

Side-by-Side Comparison

void main() {
  // const: Compile-time constant (value must be known at compile time)
  const double taxRate = 0.15;

  // final: Runtime constant (value set once, can use runtime data)
  final DateTime startTime = DateTime.now();

  // const collections are deeply immutable
  const List<String> planets = ['Mercury', 'Venus', 'Earth'];
  // planets.add('Mars');  // ERROR: Cannot modify a const list

  // final collections can be modified internally
  final List<String> fruits = ['Apple', 'Banana'];
  fruits.add('Cherry');  // OK
  print(fruits);  // [Apple, Banana, Cherry]
}

// Summary table:
// Feature         | const              | final
// ----------------|--------------------|------------------
// When set?       | Compile time       | Runtime (once)
// Reassignable?   | No                 | No
// Value known at  | Compile time       | Runtime OK
// Collections     | Deeply immutable   | Mutable contents
// Use case        | Fixed literals     | Calculated once
Tip: Use const whenever possible. It enables compiler optimizations and guarantees immutability. Use final when the value depends on something only available at runtime (like the current time, user input, or API responses).

static const -- Class-Level Constants

When you define constants inside a class, use static const to make them accessible without creating an instance. This is the standard pattern for organizing related constants.

static const in Classes

class AppConfig {
  static const String appName = 'MyDartApp';
  static const String version = '2.1.0';
  static const int maxRetries = 3;
  static const Duration timeout = Duration(seconds: 30);
}

class HttpStatus {
  static const int ok = 200;
  static const int created = 201;
  static const int badRequest = 400;
  static const int unauthorized = 401;
  static const int notFound = 404;
  static const int serverError = 500;
}

class MathConstants {
  static const double pi = 3.14159265358979;
  static const double e = 2.71828182845905;
  static const double goldenRatio = 1.61803398874989;
}

void main() {
  // Access without creating an instance
  print(AppConfig.appName);         // MyDartApp
  print(HttpStatus.notFound);       // 404
  print(MathConstants.goldenRatio); // 1.61803398874989

  if (statusCode == HttpStatus.unauthorized) {
    print('Please log in again.');
  }
}
Note: You cannot use just const (without static) at the top level of a class for instance-level constants. Class-level constants must be static const.

Basic Enums

An enum (enumeration) defines a fixed set of named values. Instead of using strings or integers to represent things like status, color, or direction, enums give you type-safe, auto-completed, and self-documenting values.

Defining and Using Enums

// Define enums outside of functions/classes
enum Direction { north, south, east, west }

enum Season { spring, summer, autumn, winter }

enum Priority { low, medium, high, critical }

void main() {
  Direction heading = Direction.north;
  Season current = Season.summer;
  Priority taskPriority = Priority.high;

  print(heading);       // Direction.north
  print(current);       // Season.summer
  print(taskPriority);  // Priority.high
}

Enum Values and Index

Every enum comes with built-in properties: index gives the zero-based position, name gives the string name, and values gives a list of all enum members.

Built-in Enum Properties

enum Color { red, green, blue, yellow }

void main() {
  // .index -- zero-based position
  print(Color.red.index);    // 0
  print(Color.green.index);  // 1
  print(Color.blue.index);   // 2

  // .name -- string name of the value
  print(Color.red.name);     // red
  print(Color.yellow.name);  // yellow

  // .values -- list of all enum members
  print(Color.values);       // [Color.red, Color.green, Color.blue, Color.yellow]
  print(Color.values.length); // 4

  // Iterate over all values
  for (var color in Color.values) {
    print('${color.name} is at index ${color.index}');
  }
  // red is at index 0
  // green is at index 1
  // blue is at index 2
  // yellow is at index 3
}

Using Enums with Switch

Enums and switch are a natural pair. The Dart analyzer warns you if you forget to handle any enum value, which prevents bugs caused by missing cases.

Enums with Switch Statements

enum TrafficLight { red, yellow, green }

String getAction(TrafficLight light) {
  switch (light) {
    case TrafficLight.red:
      return 'Stop';
    case TrafficLight.yellow:
      return 'Slow down';
    case TrafficLight.green:
      return 'Go';
  }
}

// Dart 3+ switch expression (more concise)
String getActionV2(TrafficLight light) => switch (light) {
  TrafficLight.red    => 'Stop',
  TrafficLight.yellow => 'Slow down',
  TrafficLight.green  => 'Go',
};

void main() {
  print(getAction(TrafficLight.red));     // Stop
  print(getActionV2(TrafficLight.green)); // Go
}
Tip: When switching on an enum, do not add a default case. Without it, the Dart analyzer will warn you if you add a new enum value but forget to handle it. A default silently catches new values, hiding potential bugs.

Converting Between String and Enum

You often need to convert between enums and strings, especially when reading data from APIs or databases.

String to Enum and Back

enum Status { active, inactive, banned }

void main() {
  // Enum to String
  String statusStr = Status.active.name;
  print(statusStr);  // active

  // String to Enum (Dart 2.15+)
  Status status = Status.values.byName('inactive');
  print(status);  // Status.inactive

  // Safe conversion with try-catch
  try {
    Status s = Status.values.byName('unknown');
  } catch (e) {
    print('Invalid status value: $e');
    // ArgumentError: No enum value with name "unknown"
  }

  // Safe conversion with firstWhere and a fallback
  String input = 'deleted';
  Status safeStatus = Status.values.firstWhere(
    (s) => s.name == input,
    orElse: () => Status.inactive,  // Default fallback
  );
  print(safeStatus);  // Status.inactive
}

Enhanced Enums (Dart 3+)

Dart 3 introduced enhanced enums that can have fields, constructors, methods, and even implement interfaces. This makes enums much more powerful -- each value can carry data and behavior.

Enhanced Enum with Fields and Methods

enum Planet {
  mercury(diameter: 4879, distanceFromSun: 57.9),
  venus(diameter: 12104, distanceFromSun: 108.2),
  earth(diameter: 12756, distanceFromSun: 149.6),
  mars(diameter: 6792, distanceFromSun: 227.9);

  // Fields
  final double diameter;       // km
  final double distanceFromSun; // million km

  // Constructor (must be const)
  const Planet({required this.diameter, required this.distanceFromSun});

  // Methods
  String get description =>
      '$name: ${diameter}km diameter, ${distanceFromSun}M km from Sun';

  bool get isInnerPlanet => distanceFromSun < 200;
}

void main() {
  print(Planet.earth.diameter);       // 12756
  print(Planet.earth.distanceFromSun); // 149.6
  print(Planet.earth.description);
  // earth: 12756km diameter, 149.6M km from Sun

  print(Planet.mars.isInnerPlanet);   // false

  // Filter planets
  var innerPlanets = Planet.values.where((p) => p.isInnerPlanet);
  for (var p in innerPlanets) {
    print(p.name);  // mercury, venus, earth
  }
}

Enhanced Enum -- User Roles

enum UserRole {
  admin(level: 3, label: 'Administrator'),
  editor(level: 2, label: 'Editor'),
  viewer(level: 1, label: 'Viewer'),
  guest(level: 0, label: 'Guest');

  final int level;
  final String label;

  const UserRole({required this.level, required this.label});

  bool canEdit() => level >= 2;
  bool canDelete() => level >= 3;
  bool hasHigherPrivilegeThan(UserRole other) => level > other.level;
}

void main() {
  UserRole currentUser = UserRole.editor;

  print(currentUser.label);      // Editor
  print(currentUser.canEdit());   // true
  print(currentUser.canDelete()); // false
  print(currentUser.hasHigherPrivilegeThan(UserRole.viewer)); // true

  // Use in conditional logic
  if (currentUser.canEdit()) {
    print('You can edit content.');
  }
  if (!currentUser.canDelete()) {
    print('You cannot delete content.');
  }
}

Enhanced Enum Implementing an Interface

// Enums can implement interfaces
abstract class Describable {
  String get description;
}

enum AppTheme implements Describable {
  light(primaryColor: '#FFFFFF', textColor: '#000000'),
  dark(primaryColor: '#1E1E1E', textColor: '#FFFFFF'),
  ocean(primaryColor: '#006994', textColor: '#E0F7FA');

  final String primaryColor;
  final String textColor;

  const AppTheme({required this.primaryColor, required this.textColor});

  @override
  String get description =>
      '$name theme (primary: $primaryColor, text: $textColor)';

  bool get isDark => name == 'dark';
}

void main() {
  AppTheme theme = AppTheme.ocean;
  print(theme.description);
  // ocean theme (primary: #006994, text: #E0F7FA)
  print(theme.isDark);  // false
}
Warning: Enhanced enum constructors must be const. All fields must be final. You cannot have mutable state inside an enum value.

When to Use Enums vs Constants

Both enums and constants represent fixed values, but they serve different purposes. Here is how to choose:

Choosing the Right Tool

// USE ENUMS when you have a fixed set of related choices:
enum OrderStatus { pending, processing, shipped, delivered, cancelled }
enum PaymentMethod { cash, creditCard, bankTransfer, wallet }
enum Difficulty { easy, medium, hard }

// USE CONSTANTS when you have standalone fixed values:
class ApiConfig {
  static const String baseUrl = 'https://api.example.com';
  static const int timeout = 30;
  static const String apiKey = 'abc123';
}

// USE CONSTANTS for numeric/string values that don't form a group:
const double taxRate = 0.15;
const int maxUploadSizeMB = 10;
const String defaultLanguage = 'en';

// USE ENHANCED ENUMS when each choice carries data:
enum Currency {
  usd(symbol: '\$', name: 'US Dollar'),
  eur(symbol: '\u20AC', name: 'Euro'),
  sar(symbol: '\uFDFC', name: 'Saudi Riyal');

  final String symbol;
  final String name;
  const Currency({required this.symbol, required this.name});

  String format(double amount) => '$symbol${amount.toStringAsFixed(2)}';
}

void main() {
  // Enum: Type-safe, auto-completed, exhaustive switch
  OrderStatus status = OrderStatus.shipped;

  // Constant: Simple fixed value
  print(ApiConfig.baseUrl);

  // Enhanced enum: Data + behavior
  print(Currency.usd.format(29.99));  // \$29.99
  print(Currency.sar.format(112.5));  // ﷼112.50
}

Practical Example: Status Code System

Let us build a complete example that combines constants and enhanced enums to model an HTTP response system.

Complete HTTP Response System

enum HttpMethod { get, post, put, patch, delete }

enum ResponseStatus {
  success(code: 200, message: 'OK'),
  created(code: 201, message: 'Created'),
  badRequest(code: 400, message: 'Bad Request'),
  unauthorized(code: 401, message: 'Unauthorized'),
  forbidden(code: 403, message: 'Forbidden'),
  notFound(code: 404, message: 'Not Found'),
  serverError(code: 500, message: 'Internal Server Error');

  final int code;
  final String message;

  const ResponseStatus({required this.code, required this.message});

  bool get isSuccess => code >= 200 && code < 300;
  bool get isClientError => code >= 400 && code < 500;
  bool get isServerError => code >= 500;

  static ResponseStatus fromCode(int code) {
    return ResponseStatus.values.firstWhere(
      (s) => s.code == code,
      orElse: () => ResponseStatus.serverError,
    );
  }
}

void main() {
  var status = ResponseStatus.notFound;

  print('${status.code}: ${status.message}'); // 404: Not Found
  print('Is success: ${status.isSuccess}');     // false
  print('Is client error: ${status.isClientError}'); // true

  // Look up by code
  var found = ResponseStatus.fromCode(201);
  print(found);          // ResponseStatus.created
  print(found.message);  // Created

  // Use in request handling
  void handleResponse(ResponseStatus status) {
    if (status.isSuccess) {
      print('Request succeeded!');
    } else if (status.isClientError) {
      print('Client error: ${status.message}');
    } else if (status.isServerError) {
      print('Server error -- please try again later.');
    }
  }

  handleResponse(ResponseStatus.success);    // Request succeeded!
  handleResponse(ResponseStatus.notFound);   // Client error: Not Found
}

Practice Exercise

Create an enhanced enum called TaskPriority with values low, medium, high, and urgent. Each should have:

  • An int level field (1 through 4)
  • A String label field with a human-readable name
  • A Duration responseTime field representing the expected response time
  • A method bool shouldNotify() that returns true for high and urgent
  • A method String summary() that returns a formatted string

Then write a main() function that creates a list of tasks with different priorities, filters to find tasks that should trigger notifications, and prints a summary for each.