Super Enum 💪 Dart & Flutter Tutorial – Store Custom Data

5  comments

Enumerated types in Dart have always been not so useful, compared to other languages. You cannot store additional data in them and using the regular switch statement is a pain. However, when you use a package called super_enum, you can bring a bit of the enum glory to Dart over from Kotlin.

Starter project

In this tutorial, you're going to learn about super_enum in Flutter by building states and events for a BLoC used for displaying weather forecast. It's OK if you're not at home with BLoC - we're not going to touch it much. Of course, you can use this package in any Dart project and for any occasion.

The starter project is an already functional app and basically, we're just going to move from a messy class hierarchy to a nice, generated algebraic data type. Both before and after our intervention, the app will look like this:

Adding super_enum

Since this package uses source code generation, it needs both dependencies and dev_dependencies.

pubspec.yaml

dependencies:
  flutter:
    sdk: flutter
  super_enum: ^0.2.0
  flutter_bloc: ^2.1.1
  equatable: ^1.0.1

dev_dependencies:
  flutter_test:
    sdk: flutter
  build_runner:
  super_enum_generator: ^0.2.0

Refactoring class hierarchies

The thing with plain vanilla Dart isn't that it's incapable of getting things done. It's just that writing Dart code is often times more repetitive and error-prone than it needs to be. Take the WeatherState classes for instance. How many lines of boilerplate can you spot?

Note that the amount of boilerplate is already reduced by using
Equatable for value equality. Still, there's a lot of redundant code.

main.dart

abstract class WeatherState extends Equatable {
  const WeatherState();
}

class WeatherInitial extends WeatherState {
  const WeatherInitial();
  @override
  List<Object> get props => [];
}

class WeatherLoading extends WeatherState {
  const WeatherLoading();
  @override
  List<Object> get props => [];
}

class WeatherLoaded extends WeatherState {
  final Weather weather;
  const WeatherLoaded(this.weather);
  @override
  List<Object> get props => [weather];
}

class WeatherError extends WeatherState {
  final String message;
  const WeatherError(this.message);
  @override
  List<Object> get props => [message];
}

Additionally, once we want to check through all states from the UI, there's again a lot left to be desired. Apart from the obvious boilerplate of pure if statements, it's not even exhaustive!

weather_search_page.dart

...
if (state is WeatherInitial) {
  return buildInitialInput();
} else if (state is WeatherLoading) {
  return buildLoading();
} else if (state is WeatherLoaded) {
  return buildColumnWithData(context, state.weather);
} else if (state is WeatherError) {
  return buildInitialInput();
}
...
Exhaustiveness means that you have to check through all the subclasses of, in this case, 
WeatherState.  Currently, adding another subclass wouldn't break the if-else chain, which is a problem.

Creating super enums

We can circumvent all the issues outlined above by making WeatherState into an enum and succintly annotating it while keeping boilerplate to the minimum. Behind the scenes, super_enum_generator will take the annotated enum and turn it into classes which support a Kotlin-like when method and also value equality.

Values of a super enum can be either annotated with @object when they don't hold any data or @Data when you want to pass around some data in the enum value.

weather_state.dart

import 'package:super_enum/super_enum.dart';

import '../data/model/weather.dart';

part 'weather_state.g.dart';

@superEnum
enum _WeatherState {
  @object
  Initial,
  @object
  Loading,
  @Data(fields: [
    DataField('weather', Weather),
  ])
  Loaded,
  @Data(fields: [
    DataField('message', String),
  ])
  Error,
}

After running the build command...

flutter packages pub run build_runner watch

Code in the rest of our app just broke. Before changing it all to the super_enum way of doing things, let's quickly modify WeatherEvent too, although there's just a single class right now.

weather_event.dart

import 'package:super_enum/super_enum.dart';

part 'weather_event.g.dart';

@superEnum
enum _WeatherEvent {
  @Data(fields: [
    DataField('cityName', String),
  ])
  GetWeather,
  // Other events go here...
}
Don't use super_enum for just one value in real apps 😅 Although, when you think about it, doing so can be a good choice for future-proofing when you decide to add other values later on.

Using super enums

The principle of using super_enum generated classes is simple - replace all if or switch statements by calling the when method on the enum instance directly and use factory constructors of the super type.

How does this look in practice? Let's start off by modifying the WeatherBloc. I highly encourage you to learn more about BLoC, but it's not a problem if you're not familiar with it either. The starter project contains the following code which is currently broken:

weather_bloc.dart

class WeatherBloc extends Bloc<WeatherEvent, WeatherState> {
  final WeatherRepository weatherRepository;

  WeatherBloc(this.weatherRepository);

  @override
  WeatherState get initialState => WeatherInitial();

  @override
  Stream<WeatherState> mapEventToState(
    WeatherEvent event,
  ) async* {
    // Instantiating state classes directly
    yield WeatherLoading();
    // Using type checks for determining events
    if (event is GetWeather) {
      try {
        final weather = await weatherRepository.fetchWeather(event.cityName);
        yield WeatherLoaded(weather);
      } on NetworkError {
        yield WeatherError("Couldn't fetch weather. Is the device online?");
      }
    }
  }
}

Through the power of super_enum, we're going to turn it into a more easily manageable, exhaustive code:

weather_bloc.dart

class WeatherBloc extends Bloc<WeatherEvent, WeatherState> {
  final WeatherRepository weatherRepository;

  WeatherBloc(this.weatherRepository);

  @override
  WeatherState get initialState => WeatherState.initial();

  @override
  Stream<WeatherState> mapEventToState(
    WeatherEvent event,
  ) async* {
    // Instantiating states using factories
    yield WeatherState.loading();
    // Exhaustive when "statement"
    yield* event.when(
      getWeather: (e) => mapGetWeatherToState(e),
    );
  }

  Stream<WeatherState> mapGetWeatherToState(GetWeather e) async* {
    try {
      final weather = await weatherRepository.fetchWeather(e.cityName);
      yield WeatherState.loaded(weather: weather);
    } on NetworkError {
      yield WeatherState.error(
          message: "Couldn't fetch weather. Is the device online?");
    }
  }
}

The last step is changing the UI code of the widgets which rebuild themselves based on the incoming state using the BlocBuilder. Additionally, we also show a snackbar whenever the incoming state is an Error in the BlocListener.

As you can see below, you don't need to use the exhaustive when method if it's not needed, as in the case of only showing a SnackBar when the state is Error.

weather_search_page.dart

...
import '../bloc/weather_state.dart' as weather_state;

class WeatherSearchPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      ...
      body: Container(
        ...
        child: BlocListener<WeatherBloc, WeatherState>(
          listener: (context, state) {
            // There's no point to switch through all states in the listener,
            // when all we want to do is show an error snackbar.
            if (state is weather_state.Error) {
              Scaffold.of(context).showSnackBar(
                SnackBar(
                  content: Text(state.message),
                ),
              );
            }
          },
          child: BlocBuilder<WeatherBloc, WeatherState>(
            builder: (context, state) {
              return state.when(
                initial: (_) => buildInitialInput(),
                loading: (_) => buildLoading(),
                loaded: (s) => buildColumnWithData(context, s.weather),
                error: (_) => buildInitialInput(),
              );
            },
          ),
        ),
      ),
    );
  }
  ...
}

class CityInputField extends StatelessWidget {
  ...

  void submitCityName(BuildContext context, String cityName) {
    final weatherBloc = BlocProvider.of<WeatherBloc>(context);
    weatherBloc.add(WeatherEvent.getWeather(cityName: cityName));
  }
}

And now, nothing stops you from simplifying your class hierarchies with super_enum! Not only is there less boilerplate to write, but you also get exhaustive subtype checking for free. That's what I call a good deal.

About the author 

Matt Rešetár

Matt is an app developer with a knack for teaching others. Working as a Flutter freelancer and most importantly developer educator, he doesn't have a lot of free time 😅 Yet he still manages to squeeze in tough workouts 💪 and guitar 🎸

You may also like

Flutter Custom & Staggered Page Transition Animation Tutorial

Flutter Firebase & DDD Course [5] – Sign-In Form Logic

  • Hello Matej, can you check out the later version on Super Enum and probably create a new undated version of this Tutorial.

    Thank you in advance.

    • whenPartial is my only workaround i.e

      yield* event.whenPartial(
      getWeather: (e) {
      try {
      final weather = await weatherRepository.fetchWeather(e.cityName);
      yield WeatherState.loaded(weather: weather);
      } on NetworkError {
      yield WeatherState.error(
      message: "Couldn't fetch weather. Is the device online?");
      }
      },
      );

  • I am getting an error using the latest version of super_enum package. The generated method when is of type FutureOr instead of R. Please help.

    • I don’t know if it is the cleanest approach but the following code works:

      final FutureOr result = state.when(//here goes the code for your states);
      if (result is Widget) {
      return result;
      } else {
      return Text(‘The state processing returned a future’);
      }

      • Excuse me, it has to be “FutureOr”:

        final FutureOr result = state.when(//here goes the code for your states);
        if (result is Widget) {
        return result;
        } else {
        return Text(‘The state processing returned a future’);
        }

  • {"email":"Email address invalid","url":"Website address invalid","required":"Required field missing"}
    >