Dive into Enums in Dart: From the Basics to Advanced Techniques.

Wartelski.
Stackademic
Published in
7 min readApr 15, 2024

--

Want to know more about enums in Dart? This article is for you.

Enums in Dart
Enums in Dart

Part I. Understanding Enums: Basics

Enums are great when you have a fixed number of options you want to represent.

Example of enums:
- Days of the week
- Months of the year
- Traffic light states

All these examples are more or less constant. People aren’t going to add an eighth day of the week any time soon. Right? At least I hope so. A lot of things got changed these years 😄

enum Day {
monday,
tuesday,
wednesday,
thursday,
friday,
saturday,
sunday,
}

In this example, we’ve defined an enum called Day, representing the days of the week. Each constant within the enum (monday, tuesday, etc.) is implicitly assigned an index starting from zero.

On the other hand, when a category has a frequent changes or an unlimited number of possibilities, this isn’t a great choice.

Why?

✍ Enum types are implemented as classes in Dart, and each constant value within an enum consumes memory. If there’s an unlimited number of possibilities, it could lead to excessive memory usage, especially in scenarios where enums are used extensively or when large numbers of enum instances are created.

✍ Enums are typically defined at compile time, and the compiler generates code based on the enum’s constant values. If the number of possibilities is unlimited, it’s not feasible to define all of them at compile time. This limitation can hinder the use of enums for scenarios where dynamic or runtime-generated values are required.

✍ Enums are meant to represent a finite set of related options or states. When the number of possibilities is unlimited, it becomes challenging to maintain and reason about the enum’s purpose. Code readability and maintainability suffer as a result.

To make it even more clear, let’s imagine you have some number of users. If you add another user, you’ll probably have to refactor other parts of your code. For example, if you are handling the enum cases with switch statements, you have to update all the switch statements. So rather than using enums, you’d be better off representing such data type with a class you can store in a list(or another appropriate data structure). With this approach, adding a new user type simply involves creating a new instance of the class and adding it to the list. You don’t need to modify existing code that handles user types, as the new user type will be seamlessly integrated into the existing logic.

Using Enums:

enum UserType {
admin,
regular,
guest,
}

void main() {
UserType user = UserType.regular;

switch (user) {
case UserType.admin:
print('Admin user');
break;
case UserType.regular:
print('Regular user');
break;
case UserType.guest:
print('Guest user');
break;
}
}

Using Classes:

class User {
final String name;
final bool isAdmin;

User(this.name, this.isAdmin);
}

void main() {
List<User> users = [
User('John', true),
User('Alice', false),
// Add new user
User('Bob', false),
];

for (var user in users) {
print('${user.name} ${user.isAdmin ? 'is an admin' : 'is not an admin'}');
}
}

In the class-based approach, adding a new user simply involves creating a new User instance and adding it to the users list. There’s no need to update any existing code that handles user types, making the code more maintainable and scalable.

🖍 Enums are defined at compile time, and the compiler can analyze the switch statement to ensure that all possible enum values are accounted for. That means you don’t need to use default in a switch statement as long as you’re already handling all the cases.

🖍 The compiler tells you immediately if you misspell an enum value.

Part II. Coding a Basic Enum

// Define an enum called Color
enum Color {
red,
green,
blue,
}

void main() {
// Assign an enum value to a variable
final selectedColor = Color.blue;

// Switch statement to handle enum values
switch (selectedColor) {
case Color.red:
print('Selected color is Red');
break;
case Color.green:
print('Selected color is Green');
break;
case Color.blue:
print('Selected color is Blue');
break;
}
}

By convention, you name the enum using PascalCase in which you capitalize the first character of each word. Also, use camelCase to name enum values.

! Dart recognizes that you’re handling all the enum values, so no default is necessary.

Iterating over enum values in Dart allows you to traverse through all the constants defined within the enum. This feature is particularly useful when you need to perform operations or transformations on all enum constants without explicitly specifying each one. Dart provides a built-in way to iterate over enum values using the values property of the enum.

enum Day {
monday,
tuesday,
wednesday,
thursday,
friday,
saturday,
sunday,
}

void main() {
// Iterate over enum values
for (var day in Day.values) {
print(day.name); // Output: monday, tuesday, wednesday, thursday, friday, saturday, sunday
}
}

Additionally, you can combine enum iteration with other operations, such as filtering or mapping, to perform more complex tasks:

void main() {
// Filter out weekend days
var weekdays = Day.values.where((day) => day != Day.saturday && day != Day.sunday);
print('Weekdays: $weekdays'); // Output: Weekdays: [Day.monday, Day.tuesday, Day.wednesday, Day.thursday, Day.friday]

// Map enum values to their full names
var fullNames = Day.values.map((day) => day.toString().split('.').last);
print('Full Names: $fullNames'); // Output: Full Names: [monday, tuesday, wednesday, thursday, friday, saturday, sunday]
}

Part III. Treating Enums Like Classes

Enums are just classes, and enum values are instances of the class.

Just as classes have constructors and properties, so do enums.

enum Color {
red('Red Color', '#FF0000'),
green('Green Color', '#00FF00'),
blue('Blue Color', '#0000FF');

const Color(this.message, this.hexCode);

final String message;
final String hexCode;
}

void main() {
final redColor = Color.red;
final greenColor = Color.green;
final blueColor = Color.blue;

print('Red Color: ${redColor.message}, Hex Code: ${redColor.hexCode}');
print('Green Color: ${greenColor.message}, Hex Code: ${greenColor.hexCode}');
print('Blue Color: ${blueColor.message}, Hex Code: ${blueColor.hexCode}');
}

As you can see, switch statement is no longer necessary. Your enum has a message parameter, which allows you to access the message directly.

Because enums are classes, they also support operator overloading and the possibility of adding methods.

enum Month {
January,
February,
March,
April,
May,
June,
July,
August,
September,
October,
November,
December,
}

extension MonthExtensions on Month {
// Overloading the '+' operator to add months together
Month operator +(int other) {
int result = this.index + other;
result %= 12; // Ensure the result stays within 0-11 range
return Month.values[result];
}

// Overloading the '==' operator to compare months
bool operator ==(dynamic other) {
if (other is Month) {
return this.index == other.index;
}
return false;
}
}

void main() {
Month currentMonth = Month.April;
Month nextMonth = currentMonth + 1;
Month december = Month.December;

print('Current month: $currentMonth');
// Output: Current month: Month.April
print('Next month: $nextMonth');
// Output: Next month: Month.May
print('Is December? ${december == Month.December}');
// Output: Is December? true
}

The index is the enumerated value of each enum value.January is 0, February is 1 and so on. Because this.index is an integer , you can add months(other) to it.

  • We overload the + operator to allow adding an integer to a Month, resulting in a new Month representing the added month.
  • We overload the == operator to compare two Month enum values for equality.

Using an extension allows us to add functionality to existing types without modifying their original definitions. In the case of enums, extensions provide a way to extend their behavior with custom methods or operators.

If you have a bunch of different enums where you’re repeating the same logic, you can simplify your code by adding a mixin.

enum PrimaryColor with Color { 
red,
green,
blue
}

enum BackgroundColor with Color {
pink,
yellow,
grey
}

mixin Color on Enum {
...
}

Using the on keyword in the mixin gave you access to the name property of the Enum class.

Usually, all the values in an enum will be of the same type. However, you might want to store different types for each enum value in certain situation.

enum Basic {
font,
weight,
size,
}

The font is “roboto” → String
The weight is 600 → int
The size is 18 → double

Each enum value is a different instance of Enum. And when different instances use different types for their values, you need generics to handle them.

enum Basic<T extends Object>{
font<String>('roboto'),
weight<int>(600),
size<double>(18.0);

const Basic(this.value);
final T value;
}

Object is the nearest common parent type of String, int and double, so the generic T type extends Object. This allows value to take any of those types.

I hope this article has helped you to understand the power of enums in Dart.

If you found this article insightful and valuable, I would appreciate it if you follow me and clap 👏 for it. Your support motivates and inspires further exploration and sharing of knowledge within the community.

Thank you for taking the time to read this article ❤️. Have a good day!

Stackademic 🎓

Thank you for reading until the end. Before you go:

--

--

Flutter developer crafting cross-platform apps for startups. Passionate about books, web development and cybersecurity. Let's explore tech together 🖥