Artur Fijał Senior Flutter developer, mentor, educator.

Deep dive into enhanced enums in Flutter 3.0

5 min read 1650

A deep dive into new Flutter enums (Flutter 3.0)

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:

What is an enum?

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.

How enums worked before Flutter 3.0

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?

How enhanced enums work in Flutter 3.0

So, what are the long-awaited Enum improvements?

Enums can have properties

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)

Enums can have methods like any class

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;
}

Generics

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.

Constraints

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.


More great articles from LogRocket:


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 are enhanced enums an improvement?

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!

How do I refactor my existing 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.

Can I use it to define a singleton?

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);

Conclusion

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.

: Full visibility into your web and mobile apps

LogRocket is a frontend application monitoring solution that lets you replay problems as if they happened in your own browser. Instead of guessing why errors happen, or asking users for screenshots and log dumps, LogRocket lets you replay the session to quickly understand what went wrong. It works perfectly with any app, regardless of framework, and has plugins to log additional context from Redux, Vuex, and @ngrx/store.

In addition to logging Redux actions and state, LogRocket records console logs, JavaScript errors, stacktraces, network requests/responses with headers + bodies, browser metadata, and custom logs. It also instruments the DOM to record the HTML and CSS on the page, recreating pixel-perfect videos of even the most complex single-page and mobile apps.

.
Artur Fijał Senior Flutter developer, mentor, educator.

Leave a Reply