Feature Generator π οΈ
A command-line interface (CLI) tool that accelerates Flutter development by generating Clean Architecture folder structures with boilerplate code for BLoC/Cubit state management.
Table of Contents π
- Installation
- Usage
- Generated Structure
- Example
- Dependencies
- Configuration
- Troubleshooting
- Contributing
- License
Installation π»
Install globally using Dart:
1. From pub.flutter-io.cn pub.flutter-io.cn
Import the package:
flutter pub get feature_generator
Then, to add the package to PATH
dart pub global activate feature_generator && dart pub global run feature_generator:_post_install
Key Features:
1- Automatic Shell Detection
Supports Bash, Zsh, and Fish.
2- Permanent PATH Configuration
Appends to the appropriate shell config file.
Usage π
1. Initialize Project
At first, run this command to install dependencies and core folder:
feature_generator install
This creates:
- Core directories (
lib/core/errors
,lib/core/use_cases
,lib/core/utils
) - Service locator (
lib/core/utils/service_locator.dart
) - Installs required dependencies
- π Automatically configures PATH - Adds
$HOME/.pub-cache/bin
to your shell configuration
Automatic PATH Configuration
The install
command now automatically:
- Detects your shell (Bash, Zsh, Fish)
- Adds the pub cache bin directory to your PATH
- Updates the appropriate config file (
.bashrc
,.zshrc
, etc.) - Skips if PATH is already configured
No need to manually run the post-install script!
2. Generate Features
feature_generator create --name <FEATURE_NAME>
3. Add Use Cases to Existing Features (Optional)
β οΈ Note: This step is optional and only needed if you want to add additional use cases to an existing feature.
Each feature created with the
create
command already includes a complete folder structure with initial use case files. Use this command only when you need to extend a feature with new business logic.
feature_generator add-usecase --feature <FEATURE_NAME> --usecase <USECASE_NAME>
Examples:
For create Auth
feature
feature_generator create --name Auth
Optional: For adding a Login
use case to the existing Auth
feature:
feature_generator add-usecase --feature Auth --usecase Login
Optional: For adding a Signup
use case to the existing Auth
feature:
feature_generator add-usecase --feature Auth --usecase Signup
The core folders (lib/core/errors
and lib/core/use_cases
) will only exist after running the install
command, never during feature creation.
Generated Structure π³
βββ core/ # Shared project components
β βββ errors/ # Custom error classes
β β βββ failure.dart # Failure type definitions
β βββ use_cases/ # Base use case classes
β β βββ use_case.dart # Abstract UseCase template
β βββ utils/ # Create getit initialize
β βββ use_case.dart # Get it Definition
β
βββ features/ # Feature modules
βββ <feature_name>/ # Generated feature name
βββ data/
β βββ data_sources/ # API/Remote data sources
β βββ models/ # Data model classes
β βββ repo/ # Repository implementations
β
βββ domain/
β βββ repositories/ # Abstract repository contracts
β βββ use_cases/ # Business logic components
β
βββ presentation/
βββ controller/ # BLoC/Cubit + State classes
βββ views/
βββ screens/ # Full page views
βββ widgets/ # Reusable components
The lib/
directory is divided into two main sections: shared utilities (core/
) and feature-specific modules (features/
). Below is a breakdown of the structure in a tabular format:
Directory Path | Purpose |
---|---|
core/errors/failure.dart |
Defines custom error types for the app. |
core/use_cases/use_case.dart |
Abstract template for use case classes. |
features/<feature_name>/data/data_sources/ |
Handles API or remote data interactions. |
features/<feature_name>/data/models/ |
Contains data model classes for serialization. |
features/<feature_name>/data/repo/ |
Implements data repository logic. |
features/<feature_name>/domain/repositories/ |
Defines abstract repository interfaces. |
features/<feature_name>/domain/use_cases/ |
Encapsulates business logic for the feature. |
features/<feature_name>/presentation/controller/ |
Manages state using BLoC or Cubit. |
features/<feature_name>/presentation/views/screens/ |
Full-page UI views for the feature. |
features/<feature_name>/presentation/views/widgets/ |
Reusable UI components. |
Key additions:
- Core directory structure shown at project root level
- Explicit paths for critical base files
- Clear separation between shared core components and feature modules
The core directory will be generated once during the first feature creation. Subsequent features will reuse these core components.
Core Components π¨
Failure Class (lib/core/errors/failure.dart
)
abstract class Failure {
final String message;
const Failure(this.message);
}
class ServerFailure extends Failure {
ServerFailure(String message) : super(message);
}
Example Code π§π»
1. Cubit File (lib/feature/auth/presentation/controller/user_profile_cubit.dart
):
@injectable
class UserProfileCubit extends Cubit<UserProfileState> {
final FetchUserProfileUseCase fetchUserProfileUseCase;
UserProfileCubit(this.fetchUserProfileUseCase)
: super(UserProfileInitial());
Future<void> loadProfile() async {
emit(UserProfileLoading());
// ... cubit logic
}
2. Repository Contract (user_profile_repository.dart
):
abstract class UserProfileRepository {
Future<Either<Failure, UserProfileModel>> getProfile();
}
3. UseCase Template (lib/core/use_cases/use_case.dart
):
import 'package:dartz/dartz.dart';
import '../Errors/failure.dart';
abstract class UseCases<Type> {
Future<Either<Failure, Type>> call();
}
abstract class UseCasesWithParamater<Type, Parameter> {
Future<Either<Failure, Type>> call(Parameter parameter);
}
4. Service Locator (lib/core/util/service_locator.dart
)
import 'package:get_it/get_it.dart';
import 'package:injectable/injectable.dart';
final getIt = GetIt.instance;
@InjectableInit()
void configureDependencies() => getIt.init();
5. Failure (lib/core/errors/failure.dart
) :
import 'package:dio/dio.dart';
class Failure {
final String message;
Failure({required this.message});
}
class ServerFailure extends Failure {
ServerFailure({required super.message});
factory ServerFailure.fromBadResponse(Response response) {
if (response.statusCode == 404) {
return ServerFailure(
message: 'Your request was not found, Please try later');
} else if (response.statusCode == 500) {
return ServerFailure(
message: 'There are errors with server, Please try later');
} else if (response.statusCode == 400 ||
response.statusCode == 401 ||
response.statusCode == 403) {
return ServerFailure(message: response.statusMessage.toString());
} else {
return ServerFailure(message: 'Please try later');
}
}
factory ServerFailure.fromDioException(DioException e) {
switch (e.type) {
case DioExceptionType.connectionTimeout:
return ServerFailure(message: 'Connection timed out');
case DioExceptionType.sendTimeout:
return ServerFailure(message: 'Connection send timed out');
case DioExceptionType.receiveTimeout:
return ServerFailure(message: 'Connection received timed out');
case DioExceptionType.badCertificate:
return ServerFailure(message: 'Bad certification error');
case DioExceptionType.badResponse:
return ServerFailure.fromBadResponse(e.response!);
case DioExceptionType.cancel:
return ServerFailure(message: 'Connection canceled');
case DioExceptionType.connectionError:
return ServerFailure(message: 'Connection Error');
case DioExceptionType.unknown:
return ServerFailure(message: 'Unknown Error');
}
}
}
6. Data Source (lib/featurs/*FeatureName*/data/data_sources/*featurename*_data_source.dart
):
import 'package:dio/dio.dart';
import 'package:injectable/injectable.dart';
import '/core/api_helper/api_endpoints.dart';
import '/core/api_helper/api_headers.dart';
import '/core/api_helper/api_helper.dart';
abstract class FeatureNameRemoteDataSource {
Future<FeatureNameModel> getFeatureName();
}
@Singleton(as: FeatureNameRemoteDataSource)
class FeatureNameRemoteDataSourceImplementation extends FeatureNameRemoteDataSource {
late FeatureNameModel FeatureNameModel;
late Response response;
final DioHelper dioHelper ;
FeatureNameRemoteDataSourceImplementation({required this.dioHelper});
@override
Future<FeatureNameModel> getFeatureName() async {
response = await dioHelper.getData(ApisEndPoints.kGetFeatureNameDataUrl,
headers: headersMapWithToken());
FeatureNameModel = FeatureNameModel.fromJson(response.data ?? {});
return FeatureNameModel;
}
}
7. Data RepoRepository (lib/featurs/*FeatureName*/data/repo/*featurename*_repo.dart
):
import 'package:dartz/dartz.dart';
import 'package:dio/dio.dart';
import 'package:injectable/injectable.dart';
import '/Core/Errors/failure.dart';
@Singleton(as: FEATURENAMERepository)
class FEATURENAMERepoImpl extends FEATURENAMERepository {
final FEATURENAMERemoteDataSource remoteDataSource;
FEATURENAMERepoImpl({required this.remoteDataSource});
@override
Future<Either<Failure, FEATURENAMEModel>> getFEATURENAME() async {
try {
FEATURENAMEModel request = await remoteDataSource.getFEATURENAME();
return right(request);
} on Exception catch (e) {
if (e is DioException) {
return left(ServerFailure.fromDioException(e));
} else {
return left(ServerFailure(message: e.toString()));
}
}
}
}
8. Domain Repository (lib/featurs/*FeatureName*/domain/repositories/*featurename*_repository.dart
):
import 'package:dartz/dartz.dart';
import '/Core/Errors/failure.dart';
abstract class FEATURENAMERepository {
Future<Either<Failure, FEATURENAMEModel>> getFEATURENAME();
}
9. Domain UseCases (lib/featurs/*FeatureName*/domain/use_cases/*featurename*_use_case_.dart
):
import 'package:dartz/dartz.dart';
import 'package:injectable/injectable.dart';
import '/Core/Errors/failure.dart';
import '/Core/UseCase/use_case.dart';
@lazySingleton
class FetchFEATURENAMEUseCase extends UseCases<FEATURENAMEModel> {
final FEATURENAMERepository FEATURENAMERepository;
FetchFEATURENAMEUseCase({required this.FEATURENAMERepository});
@override
Future<Either<Failure, FEATURENAMEModel>> call() async {
return await FEATURENAMERepository.getFEATURENAME();
}
}
Adding Use Cases to Existing Features π§
You can add additional use cases to an existing feature without recreating the entire feature structure. This is useful when you want to extend a feature with new business logic.
Command Usage
feature_generator add-usecase --feature <FEATURE_NAME> --usecase <USECASE_NAME>
Example: Adding Multiple Use Cases to Auth Feature
# Create the Auth feature first
feature_generator create --name Auth
# Add Login use case
feature_generator add-usecase --feature Auth --usecase Login
# Add Signup use case
feature_generator add-usecase --feature Auth --usecase Signup
# Add ResetPassword use case
feature_generator add-usecase --feature Auth --usecase ResetPassword
What Gets Generated
When you add a use case, the tool automatically creates individual files for each use case:
- Creates the use case file -
lib/features/auth/domain/use_cases/login_use_case.dart
- Creates standalone model -
lib/features/auth/data/models/login_auth_model.dart
(no entity dependency) - Creates/reuses data source -
lib/features/auth/data/data_sources/auth_data_source.dart
- Creates/reuses repository implementation -
lib/features/auth/data/repo/auth_repo.dart
- Creates/reuses domain repository interface -
lib/features/auth/domain/repositories/auth_repository.dart
Key Features:
- β No Entity Files Created - Models are standalone with full JSON serialization
- β Individual Files - Each use case gets separate files, not updates to existing ones
- β Shared Infrastructure - Data sources and repositories are created once and reused
- β Clean Architecture - Maintains proper dependency flow without entity layer complexity
Generated Files Example
For feature_generator add-usecase --feature Auth --usecase Login
:
login_use_case.dart
import 'package:dartz/dartz.dart';
import 'package:injectable/injectable.dart';
import '/core/errors/failure.dart';
import '/core/use_cases/use_case.dart';
import '../repositories/auth_repository.dart';
import '../../data/models/login_auth_model.dart';
@lazySingleton
class LoginAuthUseCase extends UseCases<LoginAuthModel> {
final AuthRepository authRepository;
LoginAuthUseCase({required this.authRepository});
@override
Future<Either<Failure, LoginAuthModel>> call() async {
return await authRepository.login();
}
}
login_auth_model.dart (Standalone - No Entity Dependency)
/// Data model for Login Auth
///
/// Standalone model with JSON serialization (no entity dependency)
class LoginAuthModel {
final int? id;
final String? message;
final bool? success;
final dynamic data;
const LoginAuthModel({
this.id,
this.message,
this.success,
this.data,
});
/// Creates a model from JSON
factory LoginAuthModel.fromJson(Map<String, dynamic> json) {
return LoginAuthModel(
id: json['id'] as int?,
message: json['message'] as String?,
success: json['success'] as bool?,
data: json['data'],
);
}
/// Converts the model to JSON
Map<String, dynamic> toJson() {
return {
'id': id,
'message': message,
'success': success,
'data': data,
};
}
/// Creates a copy with updated fields
LoginAuthModel copyWith({
int? id,
String? message,
bool? success,
dynamic data,
}) {
return LoginAuthModel(
id: id ?? this.id,
message: message ?? this.message,
success: success ?? this.success,
data: data ?? this.data,
);
}
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
return other is LoginAuthModel &&
other.id == id &&
other.message == message &&
other.success == success &&
other.data == data;
}
@override
int get hashCode {
return id.hashCode ^
message.hashCode ^
success.hashCode ^
data.hashCode;
}
@override
String toString() {
return 'LoginAuthModel(id: $id, message: $message, success: $success, data: $data)';
}
}
auth_data_source.dart (Shared Infrastructure)
import 'package:dio/dio.dart';
import 'package:injectable/injectable.dart';
//TODO: Importing the write endpoints
// import '/core/api_helper/api_endpoints.dart';
// import '/core/api_helper/api_headers.dart';
// import '/core/api_helper/api_helper.dart';
abstract class AuthRemoteDataSource {
Future<AuthModel> getAuth();
}
@Singleton(as: AuthRemoteDataSource)
class AuthRemoteDataSourceImplementation extends AuthRemoteDataSource {
late AuthModel authModel;
late Response response;
final DioHelper dioHelper;
AuthRemoteDataSourceImplementation({required this.dioHelper});
@override
Future<AuthModel> getAuth() async {
response = await dioHelper.getData(ApisEndPoints.kGetAuthDataUrl,
headers: headersMapWithToken());
authModel = AuthModel.fromJson(response.data ?? {});
return authModel;
}
}
auth_repo.dart (Shared Repository Implementation)
import 'package:dartz/dartz.dart';
import 'package:dio/dio.dart';
import 'package:injectable/injectable.dart';
import '/core/errors/failure.dart';
@Singleton(as: AuthRepository)
class AuthRepoImpl extends AuthRepository {
final AuthRemoteDataSource remoteDataSource;
AuthRepoImpl({required this.remoteDataSource});
@override
Future<Either<Failure, AuthModel>> getAuth() async {
try {
AuthModel request = await remoteDataSource.getAuth();
return right(request);
} on Exception catch (e) {
if (e is DioException) {
return left(ServerFailure.fromDioException(e));
} else {
return left(ServerFailure(message: e.toString()));
}
}
}
}
auth_repository.dart (Shared Domain Repository Interface)
import 'package:dartz/dartz.dart';
import '/core/errors/failure.dart';
abstract class AuthRepository {
Future<Either<Failure, AuthModel>> getAuth();
}
Multiple Use Cases Example
When you add multiple use cases to the same feature:
feature_generator add-usecase --feature Auth --usecase Login
feature_generator add-usecase --feature Auth --usecase Signup
Result:
- β
login_use_case.dart
+login_auth_model.dart
- β
signup_use_case.dart
+signup_auth_model.dart
- β Shared infrastructure files (data source, repositories) created once
- β No entity directory or files created (entity-free architecture)
This approach provides:
- Faster Development - No need to create entity boilerplate
- Simpler Architecture - Direct model usage without entity mapping
- Individual Use Cases - Each use case is self-contained
- Shared Infrastructure - Efficient reuse of data layer components
Dependencies π¦
These dependencies will added to your pubspec.yaml:
dependencies:
flutter_bloc:
injectable:
dartz:
dio:
get_it:
dev_dependencies:
build_runner:
injectable_generator:
Run after code generation:
flutter pub run build_runner build --delete-conflicting-outputs
Configuration βοΈ
Create feature_config.json for custom templates:
{
"base_path": "lib/modules",
"use_freezed": true,
"add_routing": false
}
Troubleshooting π§
Issue: Command not found
# Verify installation
dart pub global list
# Check PATH configuration
echo $PATH
Issue: Missing dependencies
flutter clean
flutter pub get
Contributing π€
-
Fork the repository
-
Create feature branch (git checkout -b feature/improve-generator)
-
Commit changes (git commit -m 'Add template customization')
-
Push to branch (git push origin feature/improve-generator)
-
Open a Pull Request
License π
This project is licensed under the BSD_3 License - see the LICENSE file for details.
Libraries
- feature_generator Main
- A CLI tool for generating Clean Architecture feature structures