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?
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();
}
...
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...
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...
}
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
.
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.
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’);
}
While packages like this are intended to minimize boilerplate and make your code more readable on the paper, when it comes to commercial development, code generation packages increasing complexity of maintaining your code, forcing you and your team to follow third-party approach, which is bad by itself because your project from now on will have significantly higher learning curve and having unneeded third-party dependencies. They are breaking all KISS, DRY and YAGNI principles at once and lengthen code execution chain, which is quite noticeable when you have several packages like this one. The best practice is actually reduce the amount of packages like this in your project
Thank you for your sharing. I am worried that I lack creative ideas. It is your article that makes me full of hope. Thank you. But, I have a question, can you help me?
I don’t think the title of your article matches the content lol. Just kidding, mainly because I had some doubts after reading the article.
Your point of view caught my eye and was very interesting. Thanks. I have a question for you.
I don’t think the title of your article matches the content lol. Just kidding, mainly because I had some doubts after reading the article.
Your article helped me a lot, is there any more related content? Thanks!
Thanks for sharing. I read many of your blog posts, cool, your blog is very good. https://accounts.binance.com/pt-PT/register?ref=DB40ITMB