Flutter Genius
A productivity extension for Flutter developers using Clean Architecture and BLoC. It automates repetitive boilerplate — generating data models from entities, creating BLoC structures, and more.
Features
| Feature |
Description |
| Entity → Model |
Generate a full Freezed model from any entity class — with JSON serialization and bidirectional mappers |
| Class Separator |
Split a multi-declaration Dart file into individual files with one click — enums, classes, and mixins each get their own file |
| JSON Enum Generator |
Convert any plain Dart enum into a @JsonEnum + JsonConverter pattern with configurable case format, converter toggle, and null handling |
| BLoC Generator |
Scaffold a complete BLoC feature (Bloc, Event, State) via the right-click menu |
| Localization (AppText) |
Extract strings wrapped in AppText(...) to your JSON translation file |
| Aggressive Localization |
Scan a folder and extract all hardcoded strings to easy_localization keys |
| Size Extension |
Inject a responsive sizing extension and refactor SizedBox, Padding, and BorderRadius to fluent syntax |
Requirements
Your pubspec.yaml should include:
dependencies:
freezed_annotation: ^2.4.0
json_annotation: ^4.8.0
flutter_bloc: ^8.1.0
equatable: ^2.0.5
easy_localization: ^3.0.0
flutter_screenutil: ^5.9.0
dev_dependencies:
build_runner: ^2.4.0
freezed: ^2.4.0
json_serializable: ^6.7.0
Commands
1. Entity → Model Generator (Freezed)
Converts a Clean Architecture entity class into a complete Freezed data model with JSON serialization and bidirectional mapping extensions.
How to use:
- Open any Dart file containing an entity class (e.g.
account_entity.dart).
- Place your cursor anywhere inside the class definition.
- Run the command via either:
- Command Palette (
Ctrl+Shift+P / Cmd+Shift+P) → Flutter Genius: Generate Model from Entity
- Right-click inside the editor → Flutter Genius: Generate Model from Entity
What gets generated:
- A new model file at
data/models/<name>_model.dart (path resolved automatically from domain/entities/).
@freezed class with @JsonSerializable(explicitToJson: true, fieldRename: FieldRename.snake).
@JsonKey(fromJson: ...) helpers for id, createdAt, and updatedAt via ModelGeneratorHelper.
@ConverterName() annotations for detected enum types that have a JsonConverter.
fromJson factory.
toEntity() extension on ModelClass with recursive nested mapping.
toModel() extension on EntityClass with recursive nested mapping.
List<Model>.toEntities() and List<Entity>.toModels() helper extensions.
Bonus: If your entity is a plain Dart class (not yet Freezed), it is automatically converted to a Freezed entity in-place before the model is generated (behaviour controlled by the autoConvertToFreezed setting).
Multi-class files: If your entity file contains multiple classes or enums, the command asks whether you want to separate them first:
- Yes → separates all declarations into individual files, then generates the model for the primary entity.
- No → generates a single combined model file covering all classes in the file. Plain helper classes (e.g.
Address) are automatically mapped to their model counterparts (AddressModel).
Extension Settings
Configure persistent defaults in File > Preferences > Settings under Flutter Genius:
| Setting |
Default |
Description |
flutterGenius.entityToModel.outputPath |
domainToData |
domainToData — writes the model to data/models/ (resolved from domain/entities/). sameDirectory — places the model next to the entity file. |
flutterGenius.entityToModel.autoConvertToFreezed |
always |
always — converts plain entities to Freezed automatically. ask — shows a confirmation QuickPick. never — skips conversion. |
flutterGenius.entityToModel.fieldRename |
snake |
@JsonSerializable field rename strategy: snake, none, pascal, or kebab. |
flutterGenius.entityToModel.explicitToJson |
true |
Whether to include explicitToJson: true in @JsonSerializable. |
flutterGenius.entityToModel.generateListMappers |
true |
Whether to emit List<Model>.toEntities() / List<Entity>.toModels() helper extensions. |
flutterGenius.entityToModel.jsonKeyHelpers |
true |
Whether to add @JsonKey(fromJson: ModelGeneratorHelper.generate...) for id, createdAt, and updatedAt. |
Test Class — Complex Example
Use the class below to test the command end-to-end. It exercises nested entities, enums, DateTime, nullable fields, and lists.
Input — account_entity.dart (place in domain/entities/):
import 'package:freezed_annotation/freezed_annotation.dart';
import '../../../../config/constants/enums/app/account_type_enum.dart';
import '../../../bank/domain/entities/bank_entity.dart';
import '../../../notification/domain/entities/notification_entity.dart';
import '../../../transaction/domain/entities/transaction_entity.dart';
part 'account_entity.freezed.dart';
@freezed
abstract class AccountEntity with _$AccountEntity {
const AccountEntity._();
const factory AccountEntity({
required String id,
required String name,
required bool isDefault,
required AccountType type,
required double startingAmount,
required double currentAmount,
String? description,
String? accountNumber,
BankEntity? bank,
NotificationEntity? notification,
List<TransactionEntity>? transactions,
DateTime? createdAt,
DateTime? updatedAt,
}) = _AccountEntity;
}
Expected output — account_model.dart (generated in data/models/):
import 'package:freezed_annotation/freezed_annotation.dart';
import '../../domain/entities/account_entity.dart';
// TODO: Ensure all nested models are imported here
part 'account_model.freezed.dart';
part 'account_model.g.dart';
@freezed
abstract class AccountModel with _$AccountModel {
const AccountModel._();
@JsonSerializable(explicitToJson: true, fieldRename: FieldRename.snake)
const factory AccountModel({
@JsonKey(fromJson: ModelGeneratorHelper.generateUuidFromJson)
required String id,
required String name,
required bool isDefault,
@AccountTypeConverter() required AccountType type,
required double startingAmount,
required double currentAmount,
String? description,
String? accountNumber,
BankModel? bank,
NotificationModel? notification,
List<TransactionModel>? transactions,
@JsonKey(fromJson: ModelGeneratorHelper.generateCreatedAtFromJson)
DateTime? createdAt,
@JsonKey(fromJson: ModelGeneratorHelper.generateUpdatedAtFromJson)
DateTime? updatedAt,
}) = _AccountModel;
factory AccountModel.fromJson(Map<String, dynamic> json) =>
_$AccountModelFromJson(json);
}
// -----------------------------------------------------------------------------
// MODEL -> ENTITY
// -----------------------------------------------------------------------------
extension AccountModelMapper on AccountModel {
AccountEntity toEntity() {
return AccountEntity(
id: id,
name: name,
isDefault: isDefault,
type: type,
startingAmount: startingAmount,
currentAmount: currentAmount,
description: description,
accountNumber: accountNumber,
bank: bank?.toEntity(),
notification: notification?.toEntity(),
transactions: transactions?.map((e) => e.toEntity()).toList(),
createdAt: createdAt,
updatedAt: updatedAt,
);
}
}
// -----------------------------------------------------------------------------
// ENTITY -> MODEL
// -----------------------------------------------------------------------------
extension AccountEntityMapper on AccountEntity {
AccountModel toModel() {
return AccountModel(
id: id,
name: name,
isDefault: isDefault,
type: type,
startingAmount: startingAmount,
currentAmount: currentAmount,
description: description,
accountNumber: accountNumber,
bank: bank?.toModel(),
notification: notification?.toModel(),
transactions: transactions?.map((e) => e.toModel()).toList(),
createdAt: createdAt,
updatedAt: updatedAt,
);
}
}
// -----------------------------------------------------------------------------
// HELPER LIST MAPPERS
// -----------------------------------------------------------------------------
extension AccountModelListMapper on List<AccountModel> {
List<AccountEntity> toEntities() => map((e) => e.toEntity()).toList();
}
extension AccountEntityListMapper on List<AccountEntity> {
List<AccountModel> toModels() => map((e) => e.toModel()).toList();
}
After generating, run:
dart run build_runner build --delete-conflicting-outputs
2. Class Separator
Splits any Dart file containing multiple top-level declarations (classes, enums, mixins) into individual files.
How to use:
- Open any Dart file with multiple declarations.
- Run the command via:
- Command Palette (
Ctrl+Shift+P / Cmd+Shift+P) → Flutter Genius: Class Separator
- Right-click inside the editor → Flutter Genius: Class Separator
- A QuickPick lists every declaration in the file. Pick one of:
- A specific declaration → that one stays in the original file; all others are extracted.
- "Extract all into separate files" → the primary class (auto-detected from the file name) stays in the original file; everything else is extracted.
Routing rules:
| Declaration |
Destination |
| Enum |
lib/config/constants/enums/app_specifics/<snake_name>.dart |
| Class / Mixin |
Same directory as the original file, <snake_name>.dart |
Auto-rename (entity files only): When extracting from a file that already contains Entity-named classes, any plain helper class without an Entity suffix is automatically renamed on extraction:
class Address → class AddressEntity in address_entity.dart
- All type references in every rewritten file are updated to match.
Test Class — Class Separator Example
Use the file below to test the separator end-to-end. It covers enums, nested entity classes, plain helper classes, complex default values, and nullable types.
enum AccountStatus { active, inactive, suspended, pending }
enum Role { admin, moderator, user, guest }
class TestEntity {
final String id;
final int age;
final List<String> friends;
final List<String>? defaultFriends;
final Map<String, dynamic>? defaultGroup;
final List<Map<String, dynamic>>? defaultListedGroup;
final AccountStatus status;
final AccountStatus defaultStatus;
final SubEntity mainSubEntity;
final SubEntity? optionalSubEntity;
final Address primaryAddress;
final Address? secondaryAddress;
final ProfileSettingsEntity settings;
final List<Address> pastAddresses;
const TestEntity({
required this.id,
required this.age,
required this.friends,
this.defaultFriends = const ['a', 'b', 'c'],
this.defaultGroup = const {'a': 1, 'b': 2, 'c': 3},
this.defaultListedGroup = const [
{'a': 1, 'b': 2, 'c': 3},
{'d': 4, 'e': 5, 'f': 6},
],
required this.status,
this.defaultStatus = AccountStatus.pending,
required this.mainSubEntity,
this.optionalSubEntity,
required this.primaryAddress,
this.secondaryAddress,
required this.settings,
this.pastAddresses = const [],
});
}
class SubEntity {
final String subId;
final bool isActive;
final DateTime createdAt;
final DateTime? updatedAt;
const SubEntity({
required this.subId,
required this.isActive,
required this.createdAt,
this.updatedAt,
});
}
class ProfileSettingsEntity {
final Role userRole;
final bool emailNotificationsEnabled;
// Deeply nested custom class
final Address? billingAddress;
const ProfileSettingsEntity({
required this.userRole,
this.emailNotificationsEnabled = true,
this.billingAddress,
});
}
class Address {
final String street;
final String city;
final double latitude;
final double longitude;
final bool isVerified;
const Address({
required this.street,
required this.city,
required this.latitude,
required this.longitude,
this.isVerified = false,
});
}
Expected result when "Extract all" is chosen on test_entity.dart:
| File |
Contains |
test_entity.dart |
TestEntity (kept, imports updated) |
sub_entity.dart |
SubEntity |
profile_settings_entity.dart |
ProfileSettingsEntity |
address_entity.dart |
AddressEntity (renamed from Address) |
lib/config/constants/enums/app_specifics/account_status.dart |
enum AccountStatus |
lib/config/constants/enums/app_specifics/role.dart |
enum Role |
3. JSON Enum Generator
Converts a plain Dart enum into a @JsonEnum(alwaysCreate: true) enum with per-value @JsonValue annotations, toJson() / fromString() helpers, and an optional companion JsonConverter class — all in a single in-place rewrite.
How to use:
- Open any Dart file containing an enum.
- Place your cursor inside the enum (or let the QuickPick appear if there are multiple enums).
- Run the command via:
- Command Palette (
Ctrl+Shift+P / Cmd+Shift+P) → Flutter Genius: Generate JSON Enum
- Right-click inside the editor → Flutter Genius: Generate JSON Enum
- Answer the three QuickPick prompts (each shows your saved default at the top):
- @JsonValue format — how identifiers are serialized to JSON strings.
- Converter class — whether to emit the companion
EnumConverter.
- Null handling — what
fromJson returns when the value is null or unknown.
After generating, run:
dart run build_runner build --delete-conflicting-outputs
| Option |
Example (applePay →) |
snake_case (default) |
'apple_pay' |
camelCase |
'applePay' |
SCREAMING_SNAKE_CASE |
'APPLE_PAY' |
Title Case |
'Apple Pay' |
None |
No @JsonValue annotations |
Null Handling Options (when converter is enabled)
| Option |
Converter type |
fromJson(null) returns |
| Use first value (default) |
JsonConverter<EnumName, String?> |
first enum value |
| Choose specific default |
JsonConverter<EnumName, String?> |
chosen value |
| Return null |
JsonConverter<EnumName?, String?> |
null |
Extension Settings
Configure persistent defaults in File > Preferences > Settings under Flutter Genius:
| Setting |
Default |
Description |
flutterGenius.jsonEnum.caseFormat |
snake_case |
Default @JsonValue case format |
flutterGenius.jsonEnum.generateConverter |
true |
Generate converter class by default |
flutterGenius.jsonEnum.nullHandling |
firstValue |
Default null-handling strategy |
The saved default appears at the top of each QuickPick step marked with ✦ saved default so you can confirm it with a single keypress.
Example
Input — plain enum:
enum EmployeeApplicationStatusEnum {
pending,
selected,
rejected,
applePay,
googlePay,
}
Output — with snake_case + converter + first value default:
import 'package:json_annotation/json_annotation.dart';
part 'application_status_enum.g.dart';
@JsonEnum(alwaysCreate: true)
enum EmployeeApplicationStatusEnum {
@JsonValue('pending')
pending,
@JsonValue('selected')
selected,
@JsonValue('rejected')
rejected,
@JsonValue('apple_pay')
applePay,
@JsonValue('google_pay')
googlePay;
String toJson() => _$EmployeeApplicationStatusEnumEnumMap[this]!;
static EmployeeApplicationStatusEnum? fromString(String? value) =>
values.firstWhere(
(element) => element.name.toLowerCase() == value?.toLowerCase(),
orElse: () => values.first,
);
}
class EmployeeApplicationStatusEnumConverter
implements JsonConverter<EmployeeApplicationStatusEnum, String?> {
const EmployeeApplicationStatusEnumConverter();
@override
EmployeeApplicationStatusEnum fromJson(String? json) {
if (json == null) return EmployeeApplicationStatusEnum.values.first;
return EmployeeApplicationStatusEnum.values.firstWhere(
(e) => e.name.toLowerCase() == json.toLowerCase(),
orElse: () => EmployeeApplicationStatusEnum.values.first,
);
}
@override
String? toJson(EmployeeApplicationStatusEnum object) => object.toJson();
}
4. BLoC Generator (Freezed)
Scaffold a complete BLoC feature folder via the explorer right-click menu.
- Right-click any folder in the file explorer.
- Select Create BLoC (Freezed).
- Enter the feature name (e.g.
Login).
Creates:
login_bloc/
├── login_bloc.dart
├── login_event.dart
└── login_state.dart
Extract strings from a custom AppText widget into your JSON translation file.
- Open the Command Palette.
- Run Dart: Extract AppText to Localization.
- Scans
lib/ for AppText("String") usages.
- Adds key/value to
assets/translations/en-GB.json.
- Replaces code with
AppText(LocaleKeys.generated_key).
- Runs
easy_localization:generate automatically.
6. Aggressive Localization (Folder Scan)
Extract all hardcoded strings in a folder.
- Open the Command Palette.
- Run Dart: Extract Localization (Aggressive - Select Folder).
- Pick the folder to scan (e.g.
lib/features/profile).
- Intelligently skips assets, imports, Map keys, and technical strings.
- Replaces with
LocaleKeys.generated_key.tr().
7. Size Extension & Refactor
Inject a responsive sizing extension and refactor verbose widget code to fluent syntax.
- Open the Command Palette.
- Run Flutter: Add Size Extension & Refactor UI.
| Before |
After |
SizedBox(width: 10) |
10.horizontalGap |
SizedBox(height: 20) |
20.verticalGap |
EdgeInsets.all(16) |
16.allPadding |
EdgeInsets.symmetric(horizontal: 12) |
12.horizontalPadding |
EdgeInsets.symmetric(vertical: 8) |
8.verticalPadding |
EdgeInsets.only(bottom: 10) |
10.bottomOnly |
BorderRadius.circular(15) |
15.rounded |
RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)) |
12.roundShape |
The extension automatically adds the required import to every modified file.