9 Enhanced Enums¶
Back in Dart Apprentice: Fundamentals, you learned about basic enums. They’re a way to give names to a fixed number of options. An example would be the days of the week. Under the hood, each day has an index: monday
is 0
, tuesday
is 1
and so on through sunday
is 6
. That’s why they’re called enums. They’re enumerated values with names.
Using enums in this way has always been useful, but with Dart 2.17, they got even better. You can treat the new enhanced enums like classes. And that’s what they are. Dart enums are subclasses of the Enum
class. That means you can do many of the same things to enums that you would do with a class, including adding properties and methods, implementing interfaces and using mixins and generics.
Sound good? Get ready to enhance your enum skills then!
Reviewing the Basics¶
To get started, review what you already know about enums.
What to Use Enums For¶
Enums are great when you have a fixed number of options you want to represent. Here are some examples of that:
- Traffic light states (green, yellow, red).
- Days of the week (Monday, Tuesday, …).
- Months of the year (January, February, …).
- Audio playback states (playing, paused, …).
- Weather types (sunny, cloudy, …).
All these topics are more or less constant. People aren’t going to be adding an eighth day of the week any time soon. Similarly, there are only a finite number of audio playback states. You could argue there are an indeterminate number of weather types, but if you’ve thought through your weather app carefully, you probably have a limited set that you need to show icons for.
On the other hand, when a category has frequent changes or an unlimited number of possibilities, this isn’t a great choice for enums. Here are some examples of things you probably shouldn’t represent with an enum:
- Users
- Songs
- URLs
If you add another song to your app, you’ll probably have to refactor other parts of your code. For example, if you’re handling the enum cases with switch
statements, you have to update all the switch
statements. So rather than enums, you’d be better off representing the data types listed above with classes you can store in a list. Then, whenever you add a new user or a new song, just add a new item to the list.
Advantages of Using Enums¶
In the past, people often wrote logic like this:
const int GREEN = 0;
const int YELLOW = 1;
const int RED = 2;
void printMessage(int lightColor) {
switch (lightColor) {
case GREEN:
print('Go!');
break;
case YELLOW:
print('Slow down!');
break;
case RED:
print('Stop!');
break;
default:
print('Unrecognized option');
}
}
However, there were a few problems with this kind of logic:
- The function takes any integer, so if you had defined
int VOLUME = 2
somewhere else, there would be nothing to stop you from passing inVOLUME
to the function, even though this function has nothing to do with volume. - The compiler doesn’t know there are only three possible options, so it can’t warn you if you provide a value besides
0
,1
or2
. This requires you to handle error cases withdefault
. - Sometimes people used similar logic but with strings instead of integers. For example,
if (lightColor == 'green')
. With that method, it was easy to accidentally misspell values, such as writinggeen
instead ofgreen
.
Dart enums solve all those problems:
- Each enum has its own namespace, so there’s no way to accidentally pass in
Audio.volume
when a function only acceptsTrafficLight
enum values. - The Dart compiler is smart enough to know how many values an enum has. That means you don’t need to use a default in a
switch
statement as long as you’re already handling all the cases. Dart will also warn you if you aren’t handling an enum case. - The compiler tells you immediately if you misspell an enum value.
All in all, these features of enums make them a much better option than using integer or string constants as option markers.
Coding a Basic Enum¶
Can you write an enum for the colors of a traffic light?
Open a Dart project and add the following enum outside of main
:
enum TrafficLight {
green,
yellow,
red,
}
You use the enum
keyword followed by the enum name in upper camel case. Curly braces enclose the comma-separated enum values. Adding a comma after the last item is optional but ensures that Dart will format the list vertically.
In main
, use your TrafficLight
enum like so:
final color = TrafficLight.green;
switch (color) {
case TrafficLight.green:
print('Go!');
break;
case TrafficLight.yellow:
print('Slow down!');
break;
case TrafficLight.red:
print('Stop!');
break;
}
Dart recognizes that you’re handling all the enum values, so no default case is necessary.
Run the code above, and you’ll see Go!
printed to the console.
That was a basic enum. Enhanced enums will allow you to simplify that code a lot. Keep reading to find out how.
Treating Enums Like Classes¶
Enums are just classes, and enum values are instances of the class. This means that you can apply much of your other knowledge about classes.
Adding Constructors and Properties¶
Just as classes have constructors and properties, so do enums.
Replace the TrafficLight
enum you wrote earlier with the enhanced version:
enum TrafficLight {
green('Go!'),
yellow('Slow down!'),
red('Stop!');
const TrafficLight(this.message);
final String message;
}
Here’s what has changed:
- The enum has a
const
constructor, which it uses to set the finalmessage
field in the class. Enum constructors are alwaysconst
. green
,yellow
andred
are the only instances of theTrafficLight
enum class. They each call the constructor and set the value ofmessage
for their instance.- The last enum case, which is
red
in this example, ends with a semicolon. Commas still separate the other cases.
It’s also permissible to keep the trailing comma, but you would still need to add a semicolon:
// alternate formatting
green('Go!'),
yellow('Slow down!'),
red('Stop!'), // trailing comma
; // semicolon
The only advantage here is that you’re explicitly telling the Dart formatter to display the enum list vertically rather than horizontally. Dart seems to do that anyway in this case, even without the trailing comma, so there’s no need to add it. Carry on without making this change.
Now, your previous switch
statement is no longer necessary. Replace the code in main
with the following:
final color = TrafficLight.green;
print(color.message);
Your enum has a message
parameter, which allows you to access the message directly. No need for switch
statements. That’s much better, isn’t it?
Run your code, and you’ll see the same message as before:
Go!
Operator Overloading¶
This is a good opportunity to teach you an aspect of classes you might not know about yet. The topic is operator overloading.
As you recall, operators are symbols like the following:
- Arithmetic operators:
+
-
,*
,/
,~/
,%
- Equality and relational operators:
==
,!=
,>
,<
,>=
,<=
- Assignment operators:
=
,+=
,-=
,*=
,\=
, … - Logical operators:
!
,&&
,||
- Bitwise and shift operators:
&
,|
,^
,~
,>>
,<<
,>>>
These operators all have meanings in certain contexts. For example, when you use the +
operator with integers, Dart adds them together:
print(3 + 2); // 5
When the context is strings, +
has a different meaning:
print('a' + 'b'); // ab
In this case, Dart concatenates the two strings to produce ab
.
However, what would it mean if you tried to add users, as in user1 + user2
? In this context, Dart wouldn’t know what to do because the +
operator isn’t defined for adding User
classes.
You do have the opportunity to give your own meaning to operators when the context makes sense, though. This is called operator overloading. Many, though not all, of the operators you saw above support overloading.
The following example will show how to overload an operator in a normal class. After that, you’ll see a second example where you can apply operator overloading to enums.
Overloading an Operator in a Class¶
Create the following Point
class, which has x-y coordinates:
class Point {
const Point(this.x, this.y);
final double x;
final double y;
@override
String toString() => '($x, $y)';
}
This class can represent points on a two-dimensional coordinate system where points are in the form (x, y)
, such as (1, 4)
or (3, 2)
. The image below shows these on a graph where y
is increasing in the downward direction, the usual orientation for rendering graphics:
Implement these two points in code by writing the following in main
:
const pointA = Point(1, 4);
const pointB = Point(3, 2);
In some situations, you might want to add two points together. You’d accomplish that by first adding the x-coordinates of the two points and then the y-coordinates. The following image shows the result:
Dart doesn’t know how to add two points, but you can tell Dart how to do it by overloading the +
operator in your Point
class.
Add the following code inside Point
:
Point operator +(Point other) {
return Point(x + other.x, y + other.y);
}
Here are a few points, hehe :], to note:
- Use the
operator
keyword when you want to overload an operator. - Treat the operator as a method name. Because it is the method name. The
+
method is invoked on the first point: the point that comes before the+
. Theother
point is the point that comes after the+
. - The
return
line creates a new point with the sum of the x-coordinates and the sum of the y-coordinates.
Now you can add points!
Add the following lines to the bottom of main
:
final pointC = pointA + pointB;
print(pointC);
Run that, and you’ll see the following result:
(4.0, 6.0)
Overloading an Operator in an Enum¶
Because enums are classes, they also support operator overloading.
Take the following enum for the days of the week as an example:
enum Day {
monday,
tuesday,
wednesday,
thursday,
friday,
saturday,
sunday,
}
It might not make sense to add Monday plus Tuesday, but it sort of makes sense to say monday + 2
. That would be two days later, right? Wednesday.
Replace the comma after sunday
with a semicolon and add the +
operator overload.
Here’s the complete code:
enum Day {
monday,
tuesday,
wednesday,
thursday,
friday,
saturday,
sunday;
Day operator +(int days) {
// 1
final numberOfItems = Day.values.length;
// 2
final index = (this.index + days) % numberOfItems;
// 3
return Day.values[index];
}
}
The numbered comments have the following notes:
values
is a list of all the enum values, solength
gives you the total number of values, which is7
because there are seven days in a week.- The
index
is the enumerated value of each enum value.monday
is0
,tuesday
is1
,wednesday
is2
and so on. Becausethis.index
is an integer, you can adddays
to it. The%
modulo operator divides the result by7
and gives the remainder. This makes the newindex
never go out of bounds, no matter how largedays
is. It will start over at the beginning of the list.sunday + 1
ismonday
because(6 + 1) % 7
is0
. - Convert your newly calculated index back to an enum, and you’re good to go.
To test it out, run the following code in main
:
var day = Day.monday;
day = day + 2;
print(day.name); // wednesday
day += 4;
print(day.name); // sunday
day++;
print(day.name); // monday
Not only does the +
operator work, you get +=
and ++
for free!
Adding Methods¶
You can also add methods to an enum just as you would to a normal class. Technically, operator overloading is already adding a method, but this section will provide an additional example.
Add the following getter method to your Day
enum:
Day get next {
return this + 1;
}
Because you already implemented support for the +
operator, this method returns the next day by adding 1
to whatever value this
day is.
Try it out in main
like so:
final restDay = Day.saturday;
print(restDay.next);
Run that, and you’ll see Day.sunday
printed in the console.
Implementing Interfaces¶
Say you have the following interface that you use to serialize objects for storage in a database:
abstract class Serializable {
String serialize();
}
As a reminder, “serializing” something just means to convert an object to a basic data type, most commonly a string.
Make an enum named Weather
that implements the interface like so:
enum Weather implements Serializable {
sunny,
cloudy,
rainy;
@override
String serialize() => name;
}
serialize
directly returns the enum name, such as 'sunny'
or 'cloudy'
. The built-in name
property is already a string.
Optionally, you can also add a deserialize method to your enum to go the other direction:
static Weather deserialize(String value) {
return values.firstWhere(
(element) => element.name == value,
orElse: () => Weather.sunny,
);
}
In contrast to the higher-order method where
that you learned about in Chapter 2, “Anonymous Functions”, firstWhere
returns only a single value. By comparing the input value
to the enum name
, you convert the string back to an enum. If the string value doesn’t exist, orElse
will give you a default of Weather.sunny
.
Use the methods in main
like so:
final weather = Weather.cloudy;
String serialized = weather.serialize();
print(serialized);
Weather deserialized = Weather.deserialize(serialized);
print(deserialized);
Run this code to see the serialized string and the deserialized Weather
object:
cloudy
Weather.cloudy
This example was merely to show you the syntax of implementing an interface. You could use these same methods without the interface, and it would all still work the same. The interface is only useful if some other part of your app requires Serializable
objects.
Note
Once you’ve serialized an enum, you can never change it again. Well, you can, but you do so at your peril. Say you’ve saved a bunch of enum values as strings in the database or sent them across the network. At that point, your enums have been released to the wild. You can’t get them back because they’re stored on user devices and far-away servers. If you change your enum in the next app update and then try to deserialize the old enum strings, you’ll get mismatches and unexpected behavior. That’s another reason you don’t want to make enums out of things that change frequently.
Adding Mixins¶
If you have a bunch of different enums where you’re repeating the same logic, you can simplify your code by adding a mixin.
Here’s an example:
enum Fruit with Describer {
cherry,
peach,
banana,
}
enum Vegetable with Describer {
carrot,
broccoli,
spinach,
}
mixin Describer on Enum {
void describe() {
print('This $runtimeType is a $name.');
}
}
Now, Fruit
and Vegetable
share the describe
method. Using the on
keyword in the mixin gave you access to the name
property of the Enum
class.
Test your mixin in main
like so:
final fruit = Fruit.banana;
final vegi = Vegetable.broccoli;
fruit.describe();
vegi.describe();
Run that, and you’ll see:
This Fruit is a banana.
This Vegetable is a broccoli.
OK, you English grammarians, so it shouldn’t be “a broccoli” but just “broccoli”. The author hadn’t had dinner yet, and that’s the best example he could think of.
Using Generics¶
Normally, all the values in an enum will be of the same type. For example, in the Size
enum below, the value
of each enum item is an int
:
enum Size {
small(1),
medium(5),
large(10);
const Size(this.value);
final int value;
}
However, you might want to store different types for each enum value in certain situations. Take the following example:
enum Default {
font,
size,
weight,
}
Say you have some default values you want to associate with each enum value. The default font is “roboto”, a String
; the default size is 17.0, a double
; and the default weight is 400, an int
. 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.
Replace the Default
enum above with one that uses generics:
enum Default<T extends Object> {
font<String>('roboto'),
size<double>(17.0),
weight<int>(400);
const Default(this.value);
final T value;
}
Object
is the nearest common parent type of String
, double
and int
, so the generic T
type extends Object
. This allows value
to take any of those types. Remember that each enum value (font
, size
and weight
) is an instance of the enum class. The type in angle brackets tells the constructor the type for that instance.
Write the following in main
:
String defaultFont = Default.font.value;
double defaultSize = Default.size.value;
int defaultWeight = Default.weight.value;
Although there’s only one value
property in your enum, it resolves to a different type depending on the selected enum instance.
That wraps it up for this chapter. In the next chapter, you’ll learn how to handle errors.
Challenges¶
Before moving on, here are some challenges to test your knowledge of enhanced enums. It’s best if you try to solve them yourself, but solutions are available with the supplementary materials for this book if you get stuck.
Challenge 1: Better Days Ahead¶
In this chapter, you wrote a Day
enum with the seven days of the week.
- Override the
-
operator so you can subtract integers from enum values. - When you print the
name
of yourDay
enum, it prints the days of the week in all lowercase. It’s standard to use lower camel case for enum values, but it would be nice to use uppercase for the display name. For example,Monday
instead ofmonday
. Add adisplayName
property toDay
for that.
Challenge 2: Not Found, 404¶
Create an enum for HTTP response status codes. The enum should have properties for the code and the meaning. For example, 404
and 'Not Found'
. If you aren’t familiar with the HTTP codes, look them up online. You don’t need to cover every possible code, just a few of the common ones.
Key Points¶
- Dart enums are subclasses of
Enum
. - Enums are good for representing a fixed number of options.
- Prefer enums over strings or integers as option markers.
- Enhanced enums support constructors, properties, methods, operator overloading, interfaces, mixins and generics.
- Enums must have
const
constructors. - The enum values are instances of the enum class.
- Operator overloading allows classes to give their own definitions to some operators.