Dart Object-Oriented Programming

Enums & Enhanced Enums

45 min Lesson 9 of 24

What Are Enums?

An enum (short for enumeration) is a special type that represents a fixed set of constant values. Instead of using raw strings or integers to represent states like "active", "inactive", or "pending", enums give you type-safe, auto-completed, and compile-time verified named constants. If you misspell a string, you get a runtime bug; if you misspell an enum value, you get a compile-time error.

Dart’s enums have evolved significantly. In Dart 2, enums were simple lists of constants. Starting with Dart 2.17 (and refined in Dart 3), enums became enhanced enums that can have fields, methods, constructors, and even implement interfaces -- making them one of the most powerful features in the language.

Your First Enum

// A simple enum -- just named constants
enum Direction {
  north,
  south,
  east,
  west,
}

void main() {
  Direction heading = Direction.north;

  print(heading);           // Direction.north
  print(heading.name);      // north (the string name)
  print(heading.index);     // 0 (zero-based position)

  // All values as a list
  print(Direction.values);  // [Direction.north, Direction.south, Direction.east, Direction.west]

  // Iterate over all enum values
  for (var d in Direction.values) {
    print('${d.name} is at index ${d.index}');
  }
  // north is at index 0
  // south is at index 1
  // east is at index 2
  // west is at index 3
}
Key Properties: Every enum value automatically has two properties: .name (the string name of the constant) and .index (its zero-based position in the declaration order). The static .values list contains all constants in declaration order.

Using Enums in Switch Statements

Enums shine with switch statements because the compiler ensures you handle every case. If you add a new enum value later, the compiler tells you every switch that needs updating -- this prevents bugs.

Exhaustive Switching

enum Season {
  spring,
  summer,
  autumn,
  winter,
}

String describeWeather(Season season) {
  // Dart 3 switch expression -- exhaustive by default
  return switch (season) {
    Season.spring => 'Mild and rainy',
    Season.summer => 'Hot and sunny',
    Season.autumn => 'Cool and windy',
    Season.winter => 'Cold and snowy',
  };
  // If you remove one case, Dart gives a compile error:
  // "The type Season is not exhaustively matched"
}

void main() {
  print(describeWeather(Season.summer));  // Hot and sunny

  // Parse from string
  Season? parsed = Season.values.where(
    (s) => s.name == 'winter'
  ).firstOrNull;
  print(parsed);  // Season.winter
}
Tip: Always prefer switch expressions (Dart 3) over switch statements with enums. The compiler enforces exhaustiveness in switch expressions, meaning you cannot accidentally forget a case. This is one of the biggest advantages of enums over raw strings.

Enhanced Enums: Fields, Constructors & Methods

Starting with Dart 2.17, enums can have fields, constructors, and methods -- just like classes. This lets you attach meaningful data to each enum value instead of maintaining a separate mapping. Each enum value calls a constructor, and the fields are automatically final.

Enhanced Enum with Fields and Methods

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

  // Fields -- must be final
  final int code;
  final String message;

  // Constructor -- must be const
  const HttpStatus(this.code, this.message);

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

  // Custom toString
  @override
  String toString() => 'HTTP $code: $message';

  // Static method to find by code
  static HttpStatus? fromCode(int code) {
    return HttpStatus.values.where(
      (s) => s.code == code,
    ).firstOrNull;
  }
}

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

  print(status);              // HTTP 404: Not Found
  print(status.code);         // 404
  print(status.message);      // Not Found
  print(status.isClientError); // true
  print(status.isSuccess);    // false

  // Lookup by code
  var found = HttpStatus.fromCode(200);
  print(found);  // HTTP 200: OK

  // Use in conditional logic
  var response = HttpStatus.ok;
  if (response.isSuccess) {
    print('Request succeeded!');
  }
}
Warning: All fields in an enhanced enum must be final, and all constructors must be const. Enums are immutable by design -- you cannot change their state after creation. If you need mutable state, use a class instead.

Enums Implementing Interfaces

Enhanced enums can implement one or more interfaces, making them fit into polymorphic code. This is extremely useful when you want enum values to fulfill a contract that other classes also implement.

Enum Implementing an Interface

// A contract that anything "describable" must fulfill
abstract class Describable {
  String get label;
  String describe();
}

// A contract for things that can be serialized
abstract class Serializable {
  Map<String, dynamic> toJson();
}

enum PaymentMethod implements Describable, Serializable {
  creditCard('Credit Card', 2.5, true),
  debitCard('Debit Card', 0.5, true),
  bankTransfer('Bank Transfer', 0.0, false),
  cash('Cash', 0.0, false),
  crypto('Cryptocurrency', 1.0, false);

  final String label;
  final double feePercent;
  final bool supportsRefund;

  const PaymentMethod(this.label, this.feePercent, this.supportsRefund);

  // Implementing Describable
  @override
  String describe() => '$label (fee: ${feePercent}%)';

  // Implementing Serializable
  @override
  Map<String, dynamic> toJson() => {
    'method': name,
    'label': label,
    'fee': feePercent,
    'refundable': supportsRefund,
  };

  // Additional methods
  double calculateFee(double amount) => amount * feePercent / 100;
}

void main() {
  var method = PaymentMethod.creditCard;

  // Works as Describable
  Describable d = method;
  print(d.describe());  // Credit Card (fee: 2.5%)

  // Works as Serializable
  Serializable s = method;
  print(s.toJson());  // {method: creditCard, label: Credit Card, fee: 2.5, refundable: true}

  // Calculate fee on $100
  print(method.calculateFee(100));  // 2.5

  // Filter methods that support refunds
  var refundable = PaymentMethod.values
      .where((m) => m.supportsRefund)
      .toList();
  print(refundable);  // [PaymentMethod.creditCard, PaymentMethod.debitCard]
}

When to Use Enums vs Classes

Choosing between enums and classes is a common decision. Here is a clear guideline:

Use enums when:

  • The set of values is fixed and known at compile time (will not change at runtime)
  • Each value is a constant with immutable data
  • You want exhaustive switch checking
  • Examples: directions, days of the week, HTTP methods, app states, roles

Use classes when:

  • Values are created dynamically at runtime
  • You need mutable state
  • The set of instances is not fixed (e.g., user-created categories)
  • You need inheritance hierarchies (enums cannot extend other enums)

Enum vs Class Comparison

// GOOD: Fixed set of roles -- use enum
enum UserRole {
  admin('Administrator', ['read', 'write', 'delete', 'manage']),
  editor('Editor', ['read', 'write']),
  viewer('Viewer', ['read']);

  final String displayName;
  final List<String> permissions;

  const UserRole(this.displayName, this.permissions);

  bool hasPermission(String permission) =>
      permissions.contains(permission);
}

// BAD as enum: Dynamic categories created by users -- use class
class Category {
  final String id;
  final String name;
  String? description;  // Mutable -- enums cannot have this

  Category({required this.id, required this.name, this.description});
}

void main() {
  // Enum: known at compile time, exhaustive
  var role = UserRole.editor;
  print(role.hasPermission('write'));   // true
  print(role.hasPermission('delete'));  // false

  // Class: created dynamically
  var cat = Category(id: '1', name: 'Flutter');
  cat.description = 'Flutter development tips';  // Mutable
}

Practical Example: App State Management

Let’s build a realistic example that you might use in a Flutter app -- managing application connection states with rich behavior attached to each state.

Real-World: Connection State Enum

enum ConnectionState {
  disconnected(
    'Disconnected',
    'Not connected to the server',
    false,
    Duration(seconds: 5),
  ),
  connecting(
    'Connecting',
    'Establishing connection...',
    false,
    Duration(seconds: 10),
  ),
  connected(
    'Connected',
    'Successfully connected',
    true,
    Duration(seconds: 30),
  ),
  reconnecting(
    'Reconnecting',
    'Lost connection, attempting to reconnect...',
    false,
    Duration(seconds: 15),
  ),
  error(
    'Error',
    'Connection failed',
    false,
    Duration(seconds: 60),
  );

  final String displayName;
  final String description;
  final bool canSendData;
  final Duration retryAfter;

  const ConnectionState(
    this.displayName,
    this.description,
    this.canSendData,
    this.retryAfter,
  );

  // State transition rules
  List<ConnectionState> get allowedTransitions => switch (this) {
    disconnected => [connecting],
    connecting => [connected, error],
    connected => [disconnected, reconnecting],
    reconnecting => [connected, error, disconnected],
    error => [reconnecting, disconnected],
  };

  bool canTransitionTo(ConnectionState next) =>
      allowedTransitions.contains(next);

  // Serialize / deserialize
  static ConnectionState fromString(String name) =>
      ConnectionState.values.firstWhere(
        (s) => s.name == name,
        orElse: () => ConnectionState.disconnected,
      );
}

void main() {
  var state = ConnectionState.disconnected;

  // Check allowed transitions
  print(state.canTransitionTo(ConnectionState.connecting));  // true
  print(state.canTransitionTo(ConnectionState.connected));   // false

  // Simulate state machine
  state = ConnectionState.connecting;
  print('${state.displayName}: ${state.description}');
  // Connecting: Establishing connection...

  if (!state.canSendData) {
    print('Waiting... retry after ${state.retryAfter.inSeconds}s');
  }

  state = ConnectionState.connected;
  print('Can send data: ${state.canSendData}');  // true

  // Deserialize from stored string
  var restored = ConnectionState.fromString('error');
  print(restored.displayName);  // Error
}
Best Practice: When building state machines (connection states, order statuses, authentication flows), enhanced enums are ideal because they combine the state identity with its behavior and transition rules in a single, type-safe declaration. This eliminates scattered if/else chains and makes invalid state transitions impossible to miss.