Enums & Enhanced Enums
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
}
.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
}
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!');
}
}
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
}