Watch the video (11 min)
On this page24 sections
  1. Prerequisites
  2. Connection With Previous Lectures
  3. Learning Objectives
  4. 1. What Is an Enhanced Enum?
  5. 2. Adding a Field and a const Constructor
  6. 3. The Semicolon After the Values
  7. 4. Several Fields and Named Parameters
  8. 5. Getters on an Enum
  9. 6. Methods on an Enum
  10. 7. Overriding toString()
  11. 8. Static Members
  12. 9. Implementing an Interface
  13. 10. Using a Mixin
  14. 11. The Rules
  15. 12. A Real-World Example
  16. Code Examples
  17. Common Beginner Mistakes
  18. Best Practices
  19. Mini Challenge
  20. Practice Questions
  21. Practice Answers
  22. Quick Revision
  23. Key Takeaways
  24. What's Next?

Lecture 68 — Enhanced Enums

Category11 — Enums, Extensions & Typedefs
Lecture068 of 140
Estimated video length11 min
TargetDart 3.12.2
LevelIntermediate

A plain enum is just a list of names, so the facts about each value end up in switches spread across the program. An enhanced enum keeps those facts — fields, getters, methods — inside the enum itself.

Contents

Prerequisites · Connection · Objectives · Lesson · Code Examples · Common Mistakes · Best Practices · Mini Challenge · Practice Questions · Answers · Takeaways

Every error message in this lecture is real. Each one was produced by running the code shown on Dart 3.12.2, so you will recognize it when you meet it yourself.


Prerequisites

Students should already understand, from Lectures 052–067:

  • fields, getters, methods, and toString() overrides with @override
  • const constructors and final fields (Lecture 055)
  • static members (Lecture 058)
  • interfaces with implements (Lecture 063) and mixins with with (Lecture 064)
  • declaring enums, values, name, index, and exhaustive switch (Lecture 067)

Connection With Previous Lectures

In Lecture 067, an enum named each choice, and a switch turned a value into data:

dart
enum CoffeeSize { small, medium, large }

int priceOf(CoffeeSize size) {
  return switch (size) {
    CoffeeSize.small => 250,
    CoffeeSize.medium => 300,
    CoffeeSize.large => 350,
  };
}

void main() {
  print(priceOf(CoffeeSize.medium));
}
output
300

It works, but the price lives outside the enum. Add a label, a cup volume and a calorie count, and you need three more switches in three more places. The enum knows the names; it should also know the facts.


Learning Objectives

By the end of this lecture you will be able to:

  • Explain what makes an enum "enhanced"
  • Give each enum value data with fields and a const constructor
  • Add getters and methods to an enum
  • Override toString() on an enum
  • Add static members to an enum
  • Make an enum implement an interface and use a mixin
  • Recognize the rules enhanced enums must follow, and their real error messages
  • Model real data — sizes, planets, order statuses — with enhanced enums

1. What Is an Enhanced Enum?

An enhanced enum is an enum that also has members: fields, a constructor, getters, methods. It is still a fixed list of values — but each value can carry its own data.

Analogy — a plain enum is a list of dish names. An enhanced enum is a menu card: every dish has its price and description printed right next to its name.


2. Adding a Field and a const Constructor

Each value passes its data in parentheses. A const constructor stores it in a final field:

dart
enum CoffeeSize {
  small(250),
  medium(300),
  large(350);

  const CoffeeSize(this.price);

  final int price;
}

void main() {
  print(CoffeeSize.small.price);
  print(CoffeeSize.large.price);
}
output
250
350

small(250) calls the constructor, so small.price is 250. The price now lives with the size — no switch needed.


3. The Semicolon After the Values

When an enum has members, the list of values must come first and end with a semicolon. Forget it, and Dart reads the constructor as one more value:

dart
enum CoffeeSize {
  small(250),
  medium(300),
  large(350)

  const CoffeeSize(this.price);

  final int price;
}
output
Error: Expected '}' before this.
  const CoffeeSize(this.price);
  ^^^^^

A plain enum like enum CoffeeSize { small, medium, large } needs no semicolon, because nothing follows the values.


4. Several Fields and Named Parameters

An enum can have as many fields as it needs. Named parameters keep the values readable:

dart
enum Planet {
  mercury(mass: 3.30e23, radius: 2.44e6),
  earth(mass: 5.97e24, radius: 6.37e6),
  jupiter(mass: 1.90e27, radius: 6.99e7);

  const Planet({required this.mass, required this.radius});

  final double mass;
  final double radius;
}

void main() {
  for (var planet in Planet.values) {
    print('${planet.name}: radius ${planet.radius} m');
  }
}
output
mercury: radius 2440000.0 m
earth: radius 6370000.0 m
jupiter: radius 69900000.0 m

values, name and index from Lecture 067 all still work.


5. Getters on an Enum

A getter computes something from the value's fields — or from the value itself through this:

dart
enum CoffeeSize {
  small(250, 'S'),
  medium(300, 'M'),
  large(350, 'L');

  const CoffeeSize(this.price, this.code);

  final int price;
  final String code;

  bool get isLarge => this == CoffeeSize.large;
  String get label => '$code - Rs $price';
}

void main() {
  print(CoffeeSize.medium.label);
  print(CoffeeSize.medium.isLarge);
  print(CoffeeSize.large.isLarge);
}
output
M - Rs 300
false
true

6. Methods on an Enum

Methods work as in a class. Inside the enum, switch (this) picks behaviour per value — and it is still exhaustive:

dart
enum CoffeeSize {
  small(250),
  medium(300),
  large(350);

  const CoffeeSize(this.price);

  final int price;

  int total(int cups) => price * cups;

  int get extraShots => switch (this) {
    CoffeeSize.small => 0,
    CoffeeSize.medium => 1,
    CoffeeSize.large => 2,
  };
}

void main() {
  print(CoffeeSize.small.total(3));
  print(CoffeeSize.large.extraShots);
}
output
750
2

The switch from the Connection section has moved inside the enum, where it belongs.


7. Overriding toString()

By default, printing an enum value shows CoffeeSize.medium. Override toString() to choose the text:

dart
enum CoffeeSize {
  small(250),
  medium(300),
  large(350);

  const CoffeeSize(this.price);

  final int price;

  @override
  String toString() => '${name.toUpperCase()} (Rs $price)';
}

void main() {
  print(CoffeeSize.medium);
  print('Order: ${CoffeeSize.large}');
}
output
MEDIUM (Rs 300)
Order: LARGE (Rs 350)

8. Static Members

Static fields and methods belong to the enum as a whole. Inside the enum, values and the value names can be used directly:

dart
enum CoffeeSize {
  small(250),
  medium(300),
  large(350);

  const CoffeeSize(this.price);

  final int price;

  static const CoffeeSize standard = medium;

  static CoffeeSize bestFor(int budget) {
    var best = small;
    for (var size in values) {
      if (size.price <= budget) {
        best = size;
      }
    }
    return best;
  }
}

void main() {
  print(CoffeeSize.standard.price);
  print(CoffeeSize.bestFor(320).name);
  print(CoffeeSize.bestFor(400).name);
}
output
300
medium
large

bestFor is a lookup: it turns a budget into the right value.


9. Implementing an Interface

An enum can implement an interface, so its values fit anywhere that interface is expected:

dart
abstract class Describable {
  String describe();
}

enum CoffeeSize implements Describable {
  small(250),
  medium(300),
  large(350);

  const CoffeeSize(this.price);

  final int price;

  @override
  String describe() => 'A $name coffee costs Rs $price';
}

void printAll(List<Describable> items) {
  for (var item in items) {
    print(item.describe());
  }
}

void main() {
  printAll(CoffeeSize.values);
}
output
A small coffee costs Rs 250
A medium coffee costs Rs 300
A large coffee costs Rs 350

printAll knows nothing about coffee — only that each item can describe() itself.


10. Using a Mixin

An enum can also mix in shared behaviour with with:

dart
mixin Announcer {
  void announce() => print('Now serving: $this');
}

enum CoffeeSize with Announcer { small, medium, large }

void main() {
  CoffeeSize.large.announce();
}
output
Now serving: CoffeeSize.large

An enum cannot use extends — every enum already has a built-in parent, Enum. Writing enum CoffeeSize extends Drink gives Error: Unexpected tokens.


11. The Rules

Enum values are created once, by Dart, as constants. That is why:

  1. Every field must be final.
  2. The constructor must be const.
  3. You cannot call the constructor yourself — the values listed are the only ones.
dart
enum CoffeeSize {
  small(250),
  medium(300),
  large(350);

  const CoffeeSize(this.price);

  final int price;
}

void main() {
  var huge = CoffeeSize(500);
  print(huge);
}
output
Error: Enums can't be instantiated.
  var huge = CoffeeSize(500);
             ^^^^^^^^^^

Breaking rules 1 and 2 is shown in Common Beginner Mistakes.


12. A Real-World Example

Sara's coffee order moves through fixed stages. Each stage has a label, knows whether it is the last one, and knows what comes next:

dart
enum OrderStatus {
  placed('Order placed'),
  preparing('Being prepared'),
  ready('Ready for pickup'),
  collected('Collected');

  const OrderStatus(this.label);

  final String label;

  static const OrderStatus first = placed;

  bool get isFinished => this == OrderStatus.collected;

  OrderStatus next() => isFinished ? this : values[index + 1];

  @override
  String toString() => '${index + 1}. $label';
}

void main() {
  var status = OrderStatus.first;
  print("Sara's order:");
  while (!status.isFinished) {
    print(status);
    status = status.next();
  }
  print(status);
}
output
Sara's order:
1. Order placed
2. Being prepared
3. Ready for pickup
4. Collected

Fields, a getter, a method, a static constant and toString() work together. To add a stage, you add one line to the list — nothing else in the program changes.


Code Examples

Example 1 — One Field

dart
enum TrafficLight {
  red(30),
  yellow(5),
  green(25);

  const TrafficLight(this.seconds);

  final int seconds;
}

void main() {
  for (var light in TrafficLight.values) {
    print('${light.name}: ${light.seconds}s');
  }
}
output
red: 30s
yellow: 5s
green: 25s

Example 2 — A Computed Getter

dart
enum Planet {
  mercury(mass: 3.30e23, radius: 2.44e6),
  earth(mass: 5.97e24, radius: 6.37e6);

  const Planet({required this.mass, required this.radius});

  final double mass;
  final double radius;

  static const double g = 6.674e-11;

  double get gravity => g * mass / (radius * radius);
}

void main() {
  for (var planet in Planet.values) {
    print('${planet.name}: ${planet.gravity.toStringAsFixed(1)} m/s²');
  }
}
output
mercury: 3.7 m/s²
earth: 9.8 m/s²

Example 3 — A bool Getter

dart
enum Weekday {
  monday,
  saturday,
  sunday;

  bool get isWeekend => this == saturday || this == sunday;
}

void main() {
  print(Weekday.monday.isWeekend);
  print(Weekday.sunday.isWeekend);
}
output
false
true

An enum can have members without any fields at all.

Example 4 — A Method with a Parameter

dart
enum Currency {
  usd(280.0),
  eur(300.0);

  const Currency(this.rateInRupees);

  final double rateInRupees;

  double toRupees(double amount) => amount * rateInRupees;
}

void main() {
  print(Currency.usd.toRupees(10));
  print(Currency.eur.toRupees(2.5));
}
output
2800.0
750.0

Example 5 — Overriding toString()

dart
enum Medal {
  gold('🥇'),
  silver('🥈'),
  bronze('🥉');

  const Medal(this.icon);

  final String icon;

  @override
  String toString() => '$icon $name';
}

void main() {
  print(Medal.gold);
  print('Zayan won ${Medal.silver}');
}
output
🥇 gold
Zayan won 🥈 silver

Example 6 — Implementing an Interface

dart
abstract class Priced {
  int get price;
}

enum Ticket implements Priced {
  adult(500),
  child(250);

  const Ticket(this.price);

  @override
  final int price;
}

void main() {
  Priced ticket = Ticket.child;
  print(ticket.price);
}
output
250

A final field satisfies the interface's price getter.

Example 7 — Student Marks

dart
enum Grade {
  a(80, 'Excellent'),
  b(60, 'Good'),
  c(40, 'Pass'),
  f(0, 'Fail');

  const Grade(this.minMarks, this.label);

  final int minMarks;
  final String label;

  static Grade fromMarks(int marks) {
    for (var grade in values) {
      if (marks >= grade.minMarks) {
        return grade;
      }
    }
    return f;
  }
}

void main() {
  var marks = {'Ali': 85, 'Sara': 64, 'Zayan': 38};
  marks.forEach((student, score) {
    var grade = Grade.fromMarks(score);
    print('$student: ${grade.name.toUpperCase()} (${grade.label})');
  });
}
output
Ali: A (Excellent)
Sara: B (Good)
Zayan: F (Fail)

fromMarks checks the grades from highest to lowest — the order they are listed in.

Example 8 — Shopping Total

dart
enum CoffeeSize {
  small(250),
  medium(300),
  large(350);

  const CoffeeSize(this.price);

  final int price;

  int total(int cups) => price * cups;
}

void main() {
  var bill = CoffeeSize.large.total(2) + CoffeeSize.small.total(1);
  print('Total: Rs $bill');
}
output
Total: Rs 950

Example 9 — Bank Balance

dart
enum AccountType {
  basic(0.02),
  premium(0.05);

  const AccountType(this.interestRate);

  final double interestRate;

  double yearlyInterest(double balance) => balance * interestRate;
}

void main() {
  var balance = 10000.0;
  for (var type in AccountType.values) {
    print('${type.name}: ${type.yearlyInterest(balance)}');
  }
}
output
basic: 200.0
premium: 500.0

Example 10 — Temperature Units

dart
enum TempUnit {
  celsius('°C'),
  fahrenheit('°F');

  const TempUnit(this.symbol);

  final String symbol;

  double fromCelsius(double c) => switch (this) {
    TempUnit.celsius => c,
    TempUnit.fahrenheit => c * 9 / 5 + 32,
  };

  String show(double c) => '${fromCelsius(c)}$symbol';
}

void main() {
  print(TempUnit.celsius.show(25));
  print(TempUnit.fahrenheit.show(25));
}
output
25.0°C
77.0°F

Common Beginner Mistakes

1. Forgetting the semicolon after the last value

dart
enum CoffeeSize {
  small(250),
  medium(300),
  // large(350) // ✗ no semicolon
  large(350);

  const CoffeeSize(this.price);

  final int price;
}
output
Error: Expected '}' before this.

Values come first and end with ; whenever members follow.

2. A field that is not final

dart
enum CoffeeSize {
  small(250),
  medium(300),
  large(350);

  const CoffeeSize(this.price);

  // int price; // ✗
  final int price;
}
output
Error: Enum constructors are constant so all fields must be final.

3. A constructor without const

dart
enum CoffeeSize {
  small(250),
  medium(300),
  large(350);

  // CoffeeSize(this.price); // ✗
  const CoffeeSize(this.price);

  final int price;
}
output
Error: Generative enum constructors must be marked as 'const'.

4. Trying to create a new value

dart
void main() {
  // var huge = CoffeeSize(500); // ✗
}
output
Error: Enums can't be instantiated.

Add the value to the enum's list instead: huge(500),.

5. Trying to change a value's data

dart
void main() {
  // CoffeeSize.small.price = 200; // ✗
}
output
Error: The setter 'price' isn't defined for the type 'CoffeeSize'.

Enum data is fixed. If it must change while the program runs, it belongs in a class.


Best Practices

  • Put data that belongs to a value in the enum, instead of in switches elsewhere.
  • Use named parameters when a value has two or more fields of the same type.
  • Use a getter for anything you can compute from the fields.
  • Write switch (this) inside the enum when behaviour differs per value.
  • Override toString() when the default CoffeeSize.small is not what users should see.
  • Use an enum only for a fixed set; use a class when users can create new items.

Mini Challenge

Zayan's pizza shop sells two sizes: small with 4 slices for Rs 800, and large with 8 slices for Rs 1400. Write an enhanced enum with both fields, a getter pricePerSlice (an int, using ~/), and a toString() override, then print every size.

Your program should print exactly:

output
small: 4 slices, Rs 200 each
large: 8 slices, Rs 175 each

Try it yourself before opening the solution.

Show solution
dart
enum PizzaSize {
  small(slices: 4, price: 800),
  large(slices: 8, price: 1400);

  const PizzaSize({required this.slices, required this.price});

  final int slices;
  final int price;

  int get pricePerSlice => price ~/ slices;

  @override
  String toString() => '$name: $slices slices, Rs $pricePerSlice each';
}

void main() {
  for (var size in PizzaSize.values) {
    print(size);
  }
}
output
small: 4 slices, Rs 200 each
large: 8 slices, Rs 175 each

How it works

  1. Named parameters make it clear which number is slices and which is price.
  2. pricePerSlice is computed: 800 ~/ 4 is 200, 1400 ~/ 8 is 175.
  3. print(size) calls the overridden toString().

Practice Questions

Work through these in order — they run from beginner to application to challenge. Every question gives you exact values, so you can compare your output with the answers.

Beginner

  1. Create an enum Drink with tea(100) and coffee(250), stored in a field price. Print the price of coffee.
  2. Create an enum Size with values s, m, l and a String field label ('Small', 'Medium', 'Large'). Loop over values and print each label.
  3. Add a toString() override to Drink from question 1 so that print(Drink.tea) prints tea - Rs 100.

Application

  1. Create an enum Fruit with apple(52) and banana(89) (calories). Add a getter isHighCalorie that is true above 80. Print it for both fruits.

  2. Create an enum Ticket with adult(500) and child(250) and a method cost(int count). Print Ticket.adult.cost(3) and Ticket.child.cost(2).

  3. Add a static method cheapest() to Ticket that returns the value with the lowest price. Print its name.

  4. This enum does not compile. Find the two mistakes and fix them so it prints 5:

    dart
    enum Rating {
      low(1),
      high(5)
    
      const Rating(this.stars);
    
      int stars;
    }
    
    void main() {
      print(Rating.high.stars);
    }
  5. Write an interface Labeled with a method String label(). Make an enum Room with kitchen and bedroom implement it, returning 'Room: kitchen' and so on. Print the label of every room.

Challenge

  1. Create an enum TrafficLight with red, green, yellow and a method next() that goes red → green → yellow → red. Starting at red, print five lights in a row.
  2. Create an enum AccountType with basic(minDeposit: 500, rate: 0.02) and premium(minDeposit: 5000, rate: 0.05). Add bool canOpen(double deposit). Ali has 3000.0: print each type with true or false.

Practice Answers

Question 1 — Solution

dart
enum Drink {
  tea(100),
  coffee(250);

  const Drink(this.price);

  final int price;
}

void main() {
  print(Drink.coffee.price);
}
output
250

Question 2 — Solution

dart
enum Size {
  s('Small'),
  m('Medium'),
  l('Large');

  const Size(this.label);

  final String label;
}

void main() {
  for (var size in Size.values) {
    print(size.label);
  }
}
output
Small
Medium
Large

Question 3 — Solution

dart
enum Drink {
  tea(100),
  coffee(250);

  const Drink(this.price);

  final int price;

  @override
  String toString() => '$name - Rs $price';
}

void main() {
  print(Drink.tea);
}
output
tea - Rs 100

Question 4 — Solution

dart
enum Fruit {
  apple(52),
  banana(89);

  const Fruit(this.calories);

  final int calories;

  bool get isHighCalorie => calories > 80;
}

void main() {
  print(Fruit.apple.isHighCalorie);
  print(Fruit.banana.isHighCalorie);
}
output
false
true

Question 5 — Solution

dart
enum Ticket {
  adult(500),
  child(250);

  const Ticket(this.price);

  final int price;

  int cost(int count) => price * count;
}

void main() {
  print(Ticket.adult.cost(3));
  print(Ticket.child.cost(2));
}
output
1500
500

Question 6 — Solution

dart
enum Ticket {
  adult(500),
  child(250);

  const Ticket(this.price);

  final int price;

  static Ticket cheapest() {
    var best = values.first;
    for (var ticket in values) {
      if (ticket.price < best.price) {
        best = ticket;
      }
    }
    return best;
  }
}

void main() {
  print(Ticket.cheapest().name);
}
output
child

Question 7 — Solution

The last value needs a semicolon, and the field must be final:

dart
enum Rating {
  low(1),
  high(5);

  const Rating(this.stars);

  final int stars;
}

void main() {
  print(Rating.high.stars);
}
output
5

Question 8 — Solution

dart
abstract class Labeled {
  String label();
}

enum Room implements Labeled {
  kitchen,
  bedroom;

  @override
  String label() => 'Room: $name';
}

void main() {
  for (var room in Room.values) {
    print(room.label());
  }
}
output
Room: kitchen
Room: bedroom

Question 9 — Solution

dart
enum TrafficLight {
  red,
  green,
  yellow;

  TrafficLight next() => values[(index + 1) % values.length];
}

void main() {
  var light = TrafficLight.red;
  for (var i = 0; i < 5; i++) {
    print(light.name);
    light = light.next();
  }
}
output
red
green
yellow
red
green

% values.length wraps the index from 2 back to 0.

Question 10 — Solution

dart
enum AccountType {
  basic(minDeposit: 500, rate: 0.02),
  premium(minDeposit: 5000, rate: 0.05);

  const AccountType({required this.minDeposit, required this.rate});

  final double minDeposit;
  final double rate;

  bool canOpen(double deposit) => deposit >= minDeposit;
}

void main() {
  var deposit = 3000.0;
  for (var type in AccountType.values) {
    print('${type.name}: ${type.canOpen(deposit)}');
  }
}
output
basic: true
premium: false

Quick Revision

  1. What makes an enum "enhanced"?
  2. Where must the list of values go, and how must it end?
  3. Why must every enum field be final?
  4. What error do you get when you call an enum's constructor yourself?
  5. How does a method inside an enum choose behaviour per value?
  6. What does printing an enum value show if you do not override toString()?
  7. Which can an enum use — extends, implements, with?

Key Takeaways

  • An enhanced enum adds fields, a constructor, getters and methods to a fixed set of values.
  • Each value passes its data to a const constructor: small(250).
  • Values come first and end with a semicolon when members follow.
  • All fields are final; values are constants created by Dart, never by you.
  • Getters and methods work as in classes; switch (this) picks behaviour per value.
  • Override toString() to control how a value prints.
  • Static members, implements and with all work; extends does not.
  • Keep each value's data inside the enum, so adding a value means adding one line.

What's Next?

Lecture 69 — Extension Methods. Today we added methods to enums we wrote ourselves. But what about types we did not write — could String get a capitalize() method, or int an isPrime getter of our own? Next we will learn how extension methods add new members to existing types without touching their source code.


Video lecture

Subscribe
Placeholder video — the real recording isn't linked yet.