Enums on Steroids with Dart. Best Enum features ever!
Introduction
Also known as enums or enumerations, enumerated types are a special kind of class used to represent a fixed set of constant values. These values are used to define a collection of related constants that can be referred to by name. Dart has a well-designed enum feature that stands out compared to many programming languages. This is a strong statement, but I’ll soon explain the rationale in this article.
In Dart, all enums extend the Enum class and are treated as such. They inherit methods and properties from the Enum class, making them powerful and versatile. The Dart documentation specifies that they cannot be subclassed, implemented, mixed in, or explicitly instantiated. This ensures the integrity and immutability of the enumerated types, providing a robust framework for developers.
Note: This article assumes you have basic knowledge of Dart or at least basic programming knowledge. If you’re new to Dart, I recommend familiarizing yourself with the basics of the language, such as its syntax and core principles, to fully grasp the concepts discussed here.
Let’s take a deep dive
Dart provides two types of enums, simple and enhanced enums. Simple enums are basically available in every or almost Object Oriented programming language. Enhanced enums are one of its kind and can only be found in Dart. This is an innovation added by Dart and offers developers great flexibility and power on enums, basically Enums on Steroids. If you come from other OO programming languages you may find this a bit weird but I’ll make it clear for you in the following lines.
Simple Enums
To declare a simple enumerated type, use the enum
keyword and list the values you want to be enumerated.
Let’s see an example of simple enums:
enum Status {
initial,
loading,
failure,
success;
}
The Status
enum is used to represent the status of operations within your app. Let’s say you are fetching data from an API or performing a search operation. This enum can help represent the different states the operation transits through in a meaningful way and can be used to provide a visual feedback to users, having each status mapped to a widget.
The implementation of a simple enum in a real-world example looks like this:
//Inside your build function
return switch(status){
Status.loading => LoadingWidget(),
Status.success => SuccessWidget(),
Status.failure => FailureWidget(),
_ => InitialWidget(),
}
In the above code, each status of the operation is mapped to its corresponding widget. This leads to a more readable and maintainable code.
Dart also allows to define getters even on simple enums, giving us the flexibility to do some computations based on the enum value.
Note: Getters in Dart are special methods that provide read access to an object’s properties. They are particularly useful when you need to perform some calculations or checks before returning a value
So let’s add some getters on the Status enum.
enum Status {
initial,
loading,
failure,
success;
bool get isInitial => this == Status.initial;
bool get isLoading => this == Status.loading;
bool get isFailure => this == Status.failure;
bool get isSuccess => this == Status.success;
}
Each value in an enum has an
index
getter, which returns the zero-based position of the value in the enum declaration. For example, the first value has index 0, and the second value has index 1.
So, the above implementation can be refactored into the following:
enum Status {
initial,
loading,
failure,
success;
bool get isInitial => index == 0;
bool get isLoading => index == 1;
bool get isFailure => index == 2;
bool get isSuccess => index == 3;
}
This will amount to pretty much the same result, but we all agree that the first implementation is more readable and intuitive.
A use case of those getters in our context can be to make some condition checks in order to perform further operations as in the code below:
if(status.isSuccess){
//redirect to dashboard for example
navigateTo(AppRoute.dashboard);
} else {
handleFailure();
}
Enhanced enums
Enhanced enums are basically enums with super powers. They are an improvement over traditional enums, allowing enums to have additional features such as fields, methods, and constructors. This enhancement makes enums more powerful and flexible, enabling developers to encapsulate more complex behaviors and properties directly within the enum itself.
Here is an example that declares an enhanced enum with multiple instances, instance variables, getters:
enum AccountStatus {
locked(value: 'locked', description: 'You are locked'),
banned(value: 'banned', description: 'Banned from system'),
pending(value: 'pending', description: 'We are reviewing your status'),
active(value: 'active', description: 'You have no restrictions'),
unknown(value: 'unknown', description: 'Unknown status');
const AccountStatus({
required this.value,
required this.description,
});
final String value;
final String description;
bool get isLocked => this == AccountStatus.locked;
bool get isBanned => this == AccountStatus.banned;
bool get isPending => this == AccountStatus.pending;
bool get isActive => this == AccountStatus.active;
}
You can use such an implementation to handle the status of your users in your app. Composed in a user object you have a powerful status check system implemented with minimal yet readable code.
Most of the times those data come from the backend and we need a fallback in the case of unknown data to avoid unexpected behaviors such as errors or uncaught exceptions.
This can be handled pretty easily by adding an unknown property and a factory constructor to parse the value from the server and return the appropriate status. The unknown is returned as a fallback when no match is found. See the example below:
enum AccountStatus {
//other parts of code here
...
unknown(value: 'unknown', description: "Unknown status");
...
...
factory AccountStatus.fromString(String status) {
return AccountStatus.values.firstWhere(
(el) => el.value == status,
orElse: () => AccountStatus.unknown,
);
}
}
Now here’s a real example of using the above implementation. We can access the enum properties as we do for traditional classes. We display the user status in a user-friendly way.
final status = Account.fromString(statusFromServer);
//inside your build method
final statusInfo = switch(status){
Status.unknown => "User status is unkown",
_ => "User status is ${status.value}.$description"
}
print(statusInfo);
We can even encapsulate a widget building logic inside a method in our enum.
enum AccountStatus {
Widget accountStatusComponent(){
return switch(this){
AccountStatus.unknown => Text('User is unknown'),
_ => Text('User is ${this.value}')
};
}
}
//usage inside your build method
return status.accountStatusComponent();
Conclusion
Dart enums are special because they offer for more advanced features compared to typical enums in other programming languages. Unlike traditional enums that are often limited to being mere lists of constants, Dart enums can have fields, constructors, and methods. This capability enables the encapsulation of more complex behavior and state within the enum itself. As a result, Dart enums can hold additional data and execute functions, making them highly versatile and powerful. They are particularly useful for representing a fixed set of related constants with associated values and functionality, all while maintaining a level of complexity and flexibility that is uncommon in other programming languages.
You can learn more about Dart in the official documentation page here. I hope you’ve enjoyed this article and found it helpful.