Stop using the Freezed map/when. Use sealed class with pattern matching
Pattern matching makes the usage of map/when generated code with Freezed redundant in Dart 3.0 and higher
Freezed library enables you to create simple data classes with immutability, cloning, shared fields and a JSON conversion properties with just a couple of lines of code. The Freezed code generator generates everything else.
Till Dart 3.0, the Freezed library compensated for the lack of sealed classes with functions .map()
/ .when()
as in the following example.
import 'package:freezed_annotation/freezed_annotation.dart';
part 'main_screen_state.freezed.dart';
@freezed
abstract class MainScreenState with _$MainScreenState {
const MainScreenState._();
const factory MainScreenState.init() = Init;
const factory MainScreenState.loading() = Loading;
const factory MainScreenState.content(List<String> data) = Content;
}
Further usage of .map()/.when()
is then like:
void onStateChange(MainScreenState state) {
state.when(
init: () {},
loading: () {},
content: (List<String> data) {}
);
// OR
state.map(
init: (init) {},
loading: (loading) {},
content: (content) {}
);
}
This can be completely avoided in Dart 3.0 and higher with new keywords such as sealed
. You can take the following steps to first look into the pattern matching.
Sealed keyword with Freezed
To enable native sealed classes, add to your class sealed and that it is.
import 'package:freezed_annotation/freezed_annotation.dart';
part 'main_screen_state.freezed.dart';
// sealed in front of the main screen
@freezed
sealed class MainScreenState with _$MainScreenState {
const MainScreenState._();
const factory MainScreenState.init() = Init;
const factory MainScreenState.loading() = Loading;
const factory MainScreenState.content(List<String> data) = Content;
}
Nothing more needs to be done and you are ready to go to use the pattern matching. Be aware that the created sealed classes must be public, so if you put an underscore in front of them, you cannot use them outside of the file.
At time of writing this article, the freezed library produces sealed class logic every time, but I hope there will be a parameter for that in the close future to disable it.
Switch statement with Freezed class
The sealed classes enable you to limit the scope of operations based on the number of available classes. Dart is using for it switch
exhaustive statement. All classes must be covered.
when/map subsidies
This would be equivalent of when/map with all the classes:
switch (state) {
case Init():
// your logic for Init()
case Loading():
// your logic for Loading()
case Content():
// your logic for Content()
state.data // accessing the internal data - autocast
}
This would be equivalent of maybeWhen/maybeMap
with all the classes:
switch (state) {
case Content():
// your logic for Content()
state.data // accessing the internal data - autocast
default:
// your logic for other cases
}
Deconstruction of class and conditional cases
Dart provides you with a way to deconstruct the class immediately in the case without implicitly accessing it.
switch (state) {
case Content(data: final data):
// your logic for the data
default:
// default logic
}
Or you can even create additional conditions, which case needs to meet to be picked with the keyword when
, for example:
switch (state) {
case Content(data: final data) when data.isEmpty:
// your logic for empty content
case Content(data: final data) when data.isNotEmpty:
// your logic for non-empty content
default:
// default logic
}
You can modify the conditions to your liking and needs as in usual if-else conditions.
Returning value
There is also a simplified switch
statement, which can be used to directly return value or assign value to a variable. In this type of switch statement, it is not needed to use case
, break
or default
keywords.
final data = switch (state) {
Content(data: final data) => data,
MainScreenState() => null
};
// all cases
final data = switch (state) {
Init() => null,
Loading() => null,
Content(data: final data) => data,
};
case
and break
are replaced with =>
and default is replaced by the parent sealed class or declaring all the sealed classes.
This is the most helpful in building your widgets in Flutter or assigning picked variables in your function. Here is an example:
class MainScreen extends StatelessWidget {
final MainScreenState state;
const MainScreen({super.key, required this.state});
@override
Widget build(BuildContext context) => Scaffold(
body: switch (state) {
Content(data: final data) => ContentScreen(
data: data,
),
MainScreenState() => const LoadingScreen()
},
);
}
Thanks for reading and do not forget to follow for more!