Flutter 3.0 has introduced some long-awaited changes, chief among them the way we can use enums in Flutter, which many of us have been anxiously anticipating! This is particularly the case for those who have used more robust enums in other languages and find themselves missing them when working on Flutter projects.
Instead of using static methods, method extensions, and helper or utility classes, we can now use new built-in Flutter enum features to implement everything in one place.
It is fairly new, and as most of us are used to older methods of handling such cases, there are certainly a lot of projects that may need a refactor in this regard. In this article, I will be taking an in-depth look at what you need to know about enums in Flutter 3.0.
Jump ahead:
Enum stands for enumerated type, a type of data where only a set of predefined values exist. In Dart, Enum
is simply a special kind of class used to represent a fixed number of constant values.
Here is an example of an enum
class:
enum OperatingSystem { macOS, windows, linux }
Before we proceed, it is worth remembering an important note from the Dart docs, which I will quote here because I think it sums enums up very nicely:
All enums automatically extend the Enum class. They are also sealed, meaning they cannot be subclassed, implemented, mixed in, or otherwise explicitly instantiated.
Abstract classes and mixins can explicitly implement or extend Enum, but unless they are then implemented by or mixed into an enum declaration, no objects can actually implement the type of that class or mixin.
Let’s stick to our example above and imagine that those are platforms on which Flutter is available. We would like to have a property saying whether a specific platform type can build an app for iOS devices.
How can we add a property or method to our enum? Let’s go over the most popular solutions now.
The first solution is to create an extension method, like this:
extension OperatingSystemExtension on OperatingSystem { bool switch (this) { case OperatingSystem.macOS: return true; case OperatingSystem.windows: case OperatingSystem.linux: return false; } } }
You can alternatively move from an enum to a class with an internal constructor and static consts available as “enum” values:
class OperatingSystem { final _value; const OperatingSystem._internal(this._value); toString() => 'OperatingSystem.$_value'; static const macOS = const OperatingSystem._internal('macOS'); static const windows = const OperatingSystem._internal('windows'); static const linux = const OperatingSystem._internal('linux'); }
Finally, you can use a util or helper class with a static method, but this seems like worse code practice than the two other options, in my opinion:
class OperatingSystemHelper{ static bool canBuildForIos(OperatingSystem os){ switch(os){ case OperatingSystem.macOS: return true; case OperatingSystem.windows: case OperatingSystem.linux: return false; } }
Pretty ugly right? Seems like there’s a lot of redundant code that is separate from the enum itself and it’s harder to maintain than it should be.
In addition, if you want to apply a mixin or an interface to provide custom sorting out of the box, then you are out of luck — you cannot do this in the old version of Enum
, as it would also have to exist separately.
Obviously, you can store everything in one file and this is not a big deal — in fact, we have lived with it for some time already, but if something can be coded better, then why not do it that way?
So, what are the long-awaited Enum
improvements?
enum OperatingSystem { macOS(true, true), windows(false, true), linux(false, true); const OperatingSystem(this.canBuildForIos, this.canBuildForAndroid); final bool canBuildForIos; final bool canBuildForAndroid; }
Much better, isn’t it? A constructor can have named or positional parameters just like any other class, as long as it is still a const
constructor.
(Note: If it’s not a const
, it won’t compile and it will give you a warning, so don’t worry)
enum OperatingSystem { macOS(true, true), windows(false, true), linux(false, true); const OperatingSystem(this.canBuildForIos, this.canBuildForAndroid); final bool canBuildForIos; final bool canBuildForAndroid; bool get isBestOperatingSystemForFlutterDevelopment => canBuildForIos && canBuildForAndroid; }
However, a generative constructor has to be const
, so effectively those may only be additional getters.
If a constructor has to be const
, then all instance variables must be final as well. Factory constructors can only return one of the fixed enum instances.
Obviously, there are more platforms that Flutter supports and there is no such thing as an objectively superior operating system for Flutter development; this is just a simplified example to showcase the feature.
Enhanced enum
looks more like a normal class, so we can now also implement interfaces and mixins.
mixin Foo { bool get canBuildForIos; } enum OperatingSystem with Foo implements Comparable<OperatingSystem>{ macOS(true, true), windows(false, true), linux(false, true); const OperatingSystem(this.canBuildForIos, this.canBuildForAndroid); final bool canBuildForAndroid; bool get isBestOperatingSystemForFlutterDevelopment => canBuildForIos && canBuildForAndroid; @override final bool canBuildForIos; @override int compareTo(OperatingSystem other) => name.length - other.name.length; }
Enhanced enums also allow us to use generics for enums, like this:
enum OperatingSystem<T> { macOS<String>('Yes I can'), windows<int>(0), linux(false); const OperatingSystem(this.canBuildForIos); final T canBuildForIos; } print(OperatingSystem.values.map((e) => e.canBuildForIos)); //reuslts in //(Yes I can, 0, false)
Type inference works, as you may have noticed with Linux.
There are some constraints that come with enhanced enums that should be noted.
Enhanced enums cannot extend other classes, as it automatically extends Enum
— which is to be expected. You also cannot override index
, hashCode
, or the equality operator, but this is to be expected; we are typically used to default enum implementations for those in any case.
To make sure it is readable, all instances have to be declared in the beginning and there has to be at least one.
So, to sum up, we can now declare the final instance of variables for our enums and getters and we can implement interfaces. Other constraints that come with it are pretty obvious if you think about them!
Why do enhanced enums have fewer limitations than static classes?
class OperatingSystem { final _value; final bool canBuildForIos; const OperatingSystem._internal(this._value, this.canBuildForIos); toString() => 'OperatingSystem.$_value'; static const macOS = const OperatingSystem._internal('macOS', true); static const windows = const OperatingSystem._internal('windows', false); static const linux = const OperatingSystem._internal('linux', false); } enum OperatingSystem { macOS(true), windows(false), linux(false); const OperatingSystem(this.canBuildForIos); final bool canBuildForIos; }
Having these two side by side, one clearly looks more readable, has less code, and is used for its intended purpose.
One side effect that may lead to some issues is the fact that if you want to have a switch statement against an instance of your enum and you use custom classes, you will not get a warning if you do not cover all the values. With enums, however, you do get a warning, and I am sure you’ve seen this a few times, already. So, it’s two points in favor of enums!
If you use extensions, then simply move whatever you have there to the enum class, as long as it is a getter
or final
property. As localizations most often depend on BuildContext
, these probably still have to live in a method extension, depending on your l10n solution.
If you use classes with an internal constructor, it’s the same story; just change the code to the new enum, unless it does not fit your needs.
It’s a fun little quirk that you can use an enhanced enum to basically create a singleton with less code than is normally required, but then the constructor has to be const
and all instance variables have to be final — so in most cases, there is no real benefit or use for such a singleton.
As such, any functions would be free of any side effects and they are therefore as useful as top-level static functions. When we look at the code style, Dart prefers top-level functions over classes with only static members.
//preferred String get usecase => "Am I am useless? true"; //enum way enum OperatingSystem { uselessSingleton(true); const OperatingSystem(this.canBuildForIos); final bool canBuildForIos; String get usecase => "Am I am useless? $canBuildForIos"; } //same result print(usecase); print(OperatingSystem.uselessSingleton.usecase);
Enhanced enums are something that the Flutter dev community has requested for a long time, and I love how they communicated their release with developers and provided an easy and intuitive solution that solves the majority of our existing pain points.
It is certainly not a complicated concept, but I think we could pay more attention to it because I keep catching myself forgetting about them! Additionally, many tutorials are out of date regarding enhanced enums, despite their many uses, so it’s always helpful to keep ourselves in tune with these changes.
I would recommend at least starting to use enhanced enums in existing projects, and refactoring enums where feasible. All constraints will produce a compile-time error and will immediately be highlighted by the IDE of your choice, so you do not have to worry about any regressions.
If you are super nerdy and want to know absolutely everything about enhanced enums and how they work behind the scenes, here is a great specification of them.
Install LogRocket via npm or script tag. LogRocket.init()
must be called client-side, not
server-side
$ npm i --save logrocket // Code: import LogRocket from 'logrocket'; LogRocket.init('app/id');
// Add to your HTML: <script src="https://cdn.lr-ingest.com/LogRocket.min.js"></script> <script>window.LogRocket && window.LogRocket.init('app/id');</script>
Hey there, want to help make our blog better?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowLearn how to implement one-way and two-way data binding in Vue.js, using v-model and advanced techniques like defineModel for better apps.
Compare Prisma and Drizzle ORMs to learn their differences, strengths, and weaknesses for data access and migrations.
It’s easy for devs to default to JavaScript to fix every problem. Let’s use the RoLP to find simpler alternatives with HTML and CSS.
Learn how to manage memory leaks in Rust, avoid unsafe behavior, and use tools like weak references to ensure efficient programs.