More powerful enums in Dart

The Super Enum package is good for Redux actions and more.

By Craig30 May 2020

Coming from the Swift world, I’d missed powerful enum types in Flutter development. But there’s a pub.dev package for that, and in this article we’ll explore how to use it to clean up your Redux actions.

Why enums?

An enum(erated) type is a great way to express a collection of associated values in a way where the language you’re using will help you write branching code that considers all possible cases. This immediately stands out as something we might want to do with our Redux actions:

enum GameAction {
    start,
    // ...a bunch of other actions...
    finish
}

The advantage of this is that we can switch over a GameAction to make sure that our reducer handles all possible actions, and the compiler will complain if we don’t:

    // Warning: missing case clause for 'finish'
    switch (action) {
    case GameAction.start:
    return state.copyWith(inProgress: true)
    }

It’s nice when the compiler can help you to manage your state!

But there’s a problem. We need to be able to pass parameters along with our actions. For example, for the finish action we want to send a finalScore parameter.

Associated values

In Swift, enums are a lot more useful, because cases can have “associated values”:

enum GameAction {
    case start
    case finish(finalScore: Int)
}

We’re still able to use switch, only this time we can extract the values we need:

switch action {
case .start:
    // ...
case .finish(let finalScore):
    // apply finalScore to state
}

It would be great to have a way to do this in Dart. Luckily, there is one!

Super Enum

The Super Enum package hosted here at pub.dev provides just the functionality we need. It allows us to create a supercharged enumerated type with associated values.

It works using code generation. Like me, perhaps you’re not comfortable with the idea of generating large bits of your codebase, but there are plenty of useful packages that work this way (especially for serialisation), so it should be a tool at your disposal.

After following the installation instructions, we’re ready to begin. Don’t forget to install the generator by adding super_enum_generator to your dev_dependencies in pubspec.yaml, and build_runner if it’s not there already.

Declaring our intention

As with many other packages that rely on build_runner, we start by defining a private type that tells the generator what we expect to see in the generated type:

import 'package:super_enum/super_enum.dart';
part "game_action.g.dart";

@superEnum
enum _GameAction {
  @object
  StartGame,

  @Data(fields: [
    DataField<int>('finalScore'),
  ])
  FinishGame
}

A few things to note:

  • we’ve declared a private enum _GameAction (private because of that underscore);
  • we’ve decorated “regular” enumeration cases with @object;
  • we’re using case names like StartGame instead of just start—I’ll explain why in the next section;
  • we’ve decorated cases that have associated values with @Data(fields: ...) and then some DataField instances that declare the name and type of associated value.

Now we run:

flutter pub run build_runner build

Inspecting the generated code

Let’s peek inside the generated file game_action.g.dart, and see what’s there:

// GENERATED CODE - DO NOT MODIFY BY HAND

part of 'game_action.dart';

Fair enough. Don’t touch!

@immutable
abstract class GameAction extends Equatable {
  const GameAction(this._type);

  factory GameAction.startGame() = StartGame;

  factory GameAction.finishGame({@required int finalScore}) = FinishGame;

You can see that for each case we specified a class has been generated that subclasses GameAction (note: no underscore). The classes themselves are declared at the end of the file, but here you can see factory constructors that produce them. And now you also see why we’ve used i.e. StartGame as a case name, since littering our namespace with type names like Start and Finish is not going to end well. You might consider making your case names even more explicit, like StartGameAction.

Moving on, we see the switch functionality that we need. If you’ve used Kotlin, it will look familiar:

  ...

  R when<R>(
      {@required FutureOr<R> Function(StartGame) startGame,
      @required FutureOr<R> Function(FinishGame) finishGame}) {
    ...
  }

  R whenOrElse<R>(
      {FutureOr<R> Function(StartGame) startGame,
      FutureOr<R> Function(FinishGame) finishGame,
      @required FutureOr<R> Function(GameAction) orElse}) {
    ...
  }

  FutureOr<void> whenPartial(
      {FutureOr<void> Function(StartGame) startGame,
      FutureOr<void> Function(FinishGame) finishGame}) {
    ...
  }

}

Assuming we have an instance of GameAction, for example created using GameAction.startGame(), we have three ways to “switch” over the cases:

  • by calling action.when(startGame: ..., finishGame: ...), passing to each parameter a block that accepts the incoming action with its parameters;
  • by calling action.whenOrElse(..., orElse: ...), optionally including blocks for each action and also including an orElse block that works like default in a regular switch statement;
  • by calling action.whenPartial(...), optionally including blocks for each statement, with no orElse.

By using the first of these, we can make sure all cases are handled, and just as before we’ll get a compiler warning if anything is missing!

Putting it into practice

Let’s say that an action has been dispatched to the store:

store.dispatch(GameAction.finishGame(finalScore: 100));

In our reducer, we obtain the next state by calling:

state = (action as GameAction).when(
  startGame: (_) => state.copyWith(inProgress: true),
  finishGame: (finishGameAction) => state.next(
    finalScore: finishGameAction.finalScore, 
    inProgress: false
  )
);

Note that when returns the value it got from the block that was called. Hence we can assign the result directly to the state value!

This satisfies both of our requirements:

  • We got a finalScore out of the finish game action and applied it to our state.
  • The compiler is still helping us. If we’d neglected to handle startGame or finishGame, we’d get a warning.

What I'm working on

The Random Story Generator

Generate hilarious (and gramatically correct) random stories from easy-to-write templates.

Flutter

Spacer

Set reminders for upcoming space launches using a beautiful map-based interface.

Swift, Kotlin

Awkward!

The awkward silence detector you didn't know you needed.

Swift

Emojiboard

A handwriting keyboard for emoji powered by AI.

Swift, CoreML, Keras

trainchinese Dictionary and Flash Cards

English/Russian/Spanish-Chinese dictionary and training system.

Swift, Java, Kotlin

Chinese Challenges

Chinese test exams for beginner to intermediate learners.

Flutter

Chinese Writer

The most fun you can have learning Chinese writing.

Swift, Objective-C, Android NDK, C++

CryptoSketches

A blockchain gallery for immortal—and immutable—artworks.

Ethereum, Solidity, MarkoJS, NodeJS

Your thoughts