Code generation for selectors of class fields and enum cases that helps reduce repetitive code.
select helps generate selector methods for any class that can be used anywhere where you would use a selector-lambda and pattern-matching for Enums that can replace switch/case to be less wordy.
This package is compatible with the amazing freezed code generator.
Writing less code without a meaning helps a developer to focus on writing actual code. selectable helps you focus on writing business logic instead of distracting on things that can be automatically derived.
Selectors are widely used in Flutter and Dart. The most common places of usage are:
Iterable
transformations and especially mappingsStream
transformations- The
select
method extension from Provider - Most of the selector-widgets, such as BlocSelector
Selector functions tend to be very repetitive, in the example function (state) => state.field
, word state is repeated two times, and there are four symbols and two spaces. Overall, from 22 symbols, only 11 have a meaning, that is β state.field
.
That's a great task for automation, and that's exactly what that package does. So instead of writing (model) => model.field
, it is possible to write Model$.field
with select.
Similarly, Enums suffer from the same issues of demanding to write some code that doesn't has any meaning at all. Consider the following example:
switch (theme) {
case AppTheme.light:
print('Light!');
break;
case AppTheme.dark:
print('Dark!');
break;
case AppTheme.system:
print('System!');
break;
}
From 138 symbols, only a fraction actually contains any meaning. selectable allows to write code with the same meaning, but a lot more compact:
theme.when(
light: () => print('Light!'),
dark: () => print('Dark!'),
system: () => print('System!'),
);
That's almost twice as little symbols with a lot more meaning.
select is a code-generation package, so the typical generator-annotation-builder setup is used.
- Generator β select
- Annotations β select_annotation
- Builder β build_runner
And the pubspec.yaml
should be extended as in following:
dependencies:
select_annotation: #latest version
dev_dependencies:
build_runner: #latest version
select: #latest version
To generate selectors, two following criteria should be met:
- The class is marked with the
@selectable
annotation or the enum is marked with the@matchable
annotation. - File with the class/enum includes a generated code,
part model.select.dart;
.
The following example demonstrates a minimal useful definition of the selectable class.
import 'package:select_annotation/select_annotation.dart';
part 'user.select.dart';
@selectable
class User {
final String name;
final int age;
const User(this.name, this.age);
}
selectable can (and probably should) be used in conjunction with the freezed
generator and other annotations.
import 'package:select_annotation/select_annotation.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
part 'user.select.dart';
part 'user.freezed.dart';
@selectable
@immutable
@freezed
class User with _$User {
const factory User(String name, int age) = _User;
}
With the given User
model, a class with two static methods will be generated.
abstract class User$ {
User$._();
static String name(User user) => user.name;
static int age(User user) => user.age;
}
And that's it β those selectors can be used in any way, that a regular anonymous selector is used.
To generate matching extensions for a enum, besides including the generated code through part
directive, it must be annotated with the @matchable
annotation:
@matchable
enum AppTheme {
light,
dark,
system,
}
The following enum would generate an extension with two methods:
extension $AppThemeMatcherExtension on AppTheme {
T when<T>({required T Function() light, required T Function() dark, required T Function() system}) {
switch (this) {
case AppTheme.light:
return light();
case AppTheme.dark:
return dark();
case AppTheme.system:
return system();
}
}
T whenConst<T>({required T light, required T dark, required T system}) {
switch (this) {
case AppTheme.light:
return light;
case AppTheme.dark:
return dark;
case AppTheme.system:
return system;
}
}
}
The first one strictly replaces the switch/case, by allowing to execute only the matched case, accepting only the description of the values or actions, while the second one acts more as a type hash table, creating everything in advance and, as the name suggests, should be used with constant values.
Down below are listed the most commonly used use-cases for selectors and their versions implemented with selectable.
[User('Bob', 28)].map(User$.name);
Stream.fromIterable([User('Jade', 33)]).map(User$.age);
Widget build(BuildContext context) {
final userName = context.select(User$.name);
return ...;
}
Widget build(BuildContext context) => BlocSelector<UserBloc, UserModel, int>(
selector: User$.age
builder: ...,
);
Icon(
theme.whenConst(
light: Icons.wb_sunny,
dark: Icons.brightness_3,
system: Icons.brightness_auto,
),
);
Widget build(BuildContext context) => formStep.when(
name: () => NameForm(initial: name),
age: () => AgeForm(minimumAge: widget.minimumAge),
email: () => const EmailForm(),
);