zuraffa 3.12.0
zuraffa: ^3.12.0 copied to clipboard
AI first Flutter Clean Architecture Framework and CLI with Result types, UseCase patterns, Dependency Injection and MCP server for building type-safe, scalable apps with AI agents.
π¦ Zuraffa #
A comprehensive Clean Architecture framework for Flutter applications with Result-based error handling, type-safe failures, and minimal boilerplate.
π Documentation #
- Full Documentation - Complete guides and API reference
- Landing Page - Beautiful overview and quick start
- Github - Source code and example
- Plugin Development Guide - Build custom generation plugins
- Plugin API Reference - Plugin system APIs
What is Zuraffa? #
π¦ Zuraffa (ZΓΌrafa means Giraffe in TΓΌrkΓ§e) is a modern Flutter package that implements Clean Architecture principles with a focus on developer experience and type safety. It provides a robust set of tools for building scalable, testable, and maintainable Flutter applications.
Key Features #
- β Clean Architecture Enforced: Entity-based, Single (Responsibility) Repository, Orchestrator, and Polymorphic patterns
- β UseCase Pattern: Single-shot, streaming, and background operations
- β State Management Included: Simple state management with automatic cleanup
- β
ZFA CLI Tool: Generate boilerplate code with
zfacommand - β MCP Server: AI/IDE integration via Model Context Protocol
- β GraphQL Generation: Auto-generate queries, mutations, and subscriptions
- β
Cancellation: Cooperative cancellation with
CancelToken - β Fine-grained Rebuilds: Optimize performance with selective widget updates
- β Caching: Built-in dual datasource pattern with flexible cache policies
- β
Result Type: Type-safe error handling with
Result<T, AppFailure> - β Sealed Failures: Exhaustive pattern matching for error cases
Installation #
Add this to your package's pubspec.yaml file:
dependencies:
zuraffa: ^2.7.0
Then run:
flutter pub get
Quick Start #
1. Configure Your Project #
First, set up ZFA configuration for your project:
# Activate the CLI
dart pub global activate zuraffa
# Create configuration with defaults
zfa config init
# Optionally customize defaults
zfa config set jsonByDefault false
Configuration Options:
useZorphyByDefault- Use Zorphy for entities (default: true)jsonByDefault- Default JSON serialization (default: true)compareByDefault- Default compareTo generation (default: true)defaultEntityOutput- Entity output (default: lib/src/domain/entities)
Note: Entity generation requires
zorphy_annotationin your project. ZFA will prompt you to add it when creating your first entity, or you can add it now:dart pub add zorphy_annotation
2. Create Your Entities #
Create entities first, then generate Clean Architecture around them:
# Create an enum
zfa entity enum -n OrderStatus --value pending,processing,shipped,delivered
# Create entities with fields
zfa entity create -n User --field name:String --field email:String?
zfa entity create -n Order --field customer:\$User --field status:OrderStatus --field items:List<\$OrderItem>
zfa entity create -n OrderItem --field product:\$Product --field quantity:int --field price:double
# List all entities
zfa entity list
3. Generate Clean Architecture #
Now generate complete features around your entities:
# Generate a complete feature for your entity
zfa generate Order --methods=get,getList,create,update,delete --data --vpc --state --test
# Or generate multiple entities at once
zfa generate User --methods=get,create --data
zfa generate Product --methods=get,getList,watch,watchList --data --vpc --state
That's it! One command generates:
- β Domain layer (UseCases + Repository interface)
- β Data layer (DataRepository + DataSource)
- β Presentation layer (View, Presenter, Controller, State)
- β Unit tests for all UseCases
4. Run Code Generation #
After creating entities, run the build:
# Run Zorphy + json_serializable code generation
zfa build
# Or watch for changes
zfa build --watch
This generates:
- Entity implementations with
copyWith,==,hashCode,toString - JSON serialization (
toJson/fromJson) - Typed patch classes for updates (when using
--zorphy)
5. Use the Generated Code #
class ProductView extends CleanView {
final ProductRepository productRepository;
const ProductView({super.key, required this.productRepository});
@override
State<ProductView> createState() => _ProductViewState(
ProductController(
ProductPresenter(productRepository: productRepository),
),
);
}
class _ProductViewState extends CleanViewState<ProductView, ProductController> {
_ProductViewState(super.controller);
@override
void onInitState() {
super.onInitState();
controller.getProductList();
}
@override
Widget get view {
return Scaffold(
key: globalKey,
appBar: AppBar(title: const Text('Products')),
body: ControlledWidgetBuilder<ProductController>(
builder: (context, controller) {
if (controller.viewState.isLoading) {
return const CircularProgressIndicator();
}
return ListView.builder(
itemCount: controller.viewState.productList.length,
itemBuilder: (context, index) {
final product = controller.viewState.productList[index];
return ListTile(title: Text(product.name));
},
);
},
),
);
}
}
Generated Output Example #
β
Created 2 entities
β³ lib/src/domain/entities/order/order.dart
β³ lib/src/domain/entities/order/order.zorphy.dart
β³ lib/src/domain/entities/order/order.g.dart
β³ lib/src/domain/entities/order_item/order_item.dart
β³ lib/src/domain/entities/order_item/order_item.zorphy.dart
β³ lib/src/domain/entities/order_item/order_item.g.dart
β
Generated 21 files for Order
β³ lib/src/domain/repositories/order_repository.dart
β³ lib/src/domain/usecases/order/get_order_usecase.dart
β³ lib/src/domain/usecases/order/watch_order_usecase.dart
β³ lib/src/domain/usecases/order/create_order_usecase.dart
β³ lib/src/domain/usecases/order/update_order_usecase.dart
β³ lib/src/domain/usecases/order/delete_order_usecase.dart
β³ lib/src/domain/usecases/order/get_order_list_usecase.dart
β³ lib/src/domain/usecases/order/watch_order_list_usecase.dart
β³ lib/src/presentation/pages/order/order_presenter.dart
β³ lib/src/presentation/pages/order/order_controller.dart
β³ lib/src/presentation/pages/order/order_view.dart
β³ lib/src/presentation/pages/order/order_state.dart
β³ lib/src/data/data_sources/order/order_data_source.dart
β³ lib/src/data/repositories/data_order_repository.dart
β test/domain/usecases/order/get_order_usecase_test.dart
β test/domain/usecases/order/watch_order_usecase_test.dart
β test/domain/usecases/order/create_order_usecase_test.dart
β test/domain/usecases/order/update_order_usecase_test.dart
β test/domain/usecases/order/delete_order_usecase_test.dart
β test/domain/usecases/order/get_order_list_usecase_test.dart
β test/domain/usecases/order/watch_order_list_usecase_test.dart
π Next steps:
β’ Create a DataSource that implements ProductDataSource in data layer
β’ Register repositories with DI container
β’ Run tests: flutter test
Core Concepts #
Result Type #
All operations return Result<T, AppFailure> for type-safe error handling:
final result = await getProductUseCase('product-123');
// Pattern matching with fold
result.fold(
(product) => showProduct(product),
(failure) => showError(failure),
);
// Or use switch for exhaustive handling
switch (failure) {
case NotFoundFailure():
showNotFound();
case NetworkFailure():
showOfflineMessage();
case UnauthorizedFailure():
navigateToLogin();
default:
showGenericError();
}
AppFailure Hierarchy #
Zuraffa provides a sealed class hierarchy for comprehensive error handling:
sealed class AppFailure implements Exception {
final String message;
final StackTrace? stackTrace;
final Object? cause;
}
// Specific failure types
final class ServerFailure extends AppFailure { ... }
final class NetworkFailure extends AppFailure { ... }
final class ValidationFailure extends AppFailure { ... }
final class NotFoundFailure extends AppFailure { ... }
final class UnauthorizedFailure extends AppFailure { ... }
final class ForbiddenFailure extends AppFailure { ... }
final class TimeoutFailure extends AppFailure { ... }
final class CacheFailure extends AppFailure { ... }
final class ConflictFailure extends AppFailure { ... }
final class CancellationFailure extends AppFailure { ... }
final class UnknownFailure extends AppFailure { ... }
Data Updates #
Zuraffa supports two strategies for updating entities:
1. Flexible Partial Updates (Default)
Uses Partial<T> (a Map<String, dynamic>) to send only changed fields. The generator automatically adds validation to ensure only valid fields are updated.
// Generated UpdateUseCase
// params.validate(['id', 'name', 'price']); <-- Auto-generated from Entity
await updateProduct(id: '123', data: {'name': 'New Product Name'});
2. Typed Updates with Zorphy (--zorphy)
If you use Zorphy or similar tools, you can use typed Patch objects for full type safety.
zfa generate Product --methods=update --zorphy
// Generated with --zorphy
await updateProduct(id: '123', data: ProductPatch(name: 'New Product Name'));
UseCase Types #
Single-shot UseCase
For operations that return once:
class GetProductUseCase extends UseCase<Product, String> {
final ProductRepository _repository;
GetProductUseCase(this._repository);
@override
Future<Product> execute(String productId, CancelToken? cancelToken) async {
return _repository.getProduct(productId);
}
}
StreamUseCase
For reactive operations that emit multiple values:
class WatchProductsUseCase extends StreamUseCase<List<Product>, NoParams> {
final ProductRepository _repository;
WatchProductsUseCase(this._repository);
@override
Stream<List<Product>> execute(NoParams params, CancelToken? cancelToken) {
return _repository.watchProducts();
}
}
BackgroundUseCase
For CPU-intensive operations on isolates:
class ProcessImageUseCase extends BackgroundUseCase<ProcessedImage, ImageParams> {
@override
BackgroundTask<ImageParams> buildTask() => _processImage;
static void _processImage(BackgroundTaskContext<ImageParams> context) {
final result = applyFilters(context.params.image);
context.sendData(result);
context.sendDone();
}
}
CompletableUseCase
For operations that don't return a value (like delete, logout, or clear cache):
class DeleteProductUseCase extends CompletableUseCase<String> {
final ProductRepository _repository;
DeleteProductUseCase(this._repository);
@override
Future<void> execute(String productId, CancelToken? cancelToken) async {
cancelToken?.throwIfCancelled();
await _repository.delete(productId);
}
}
// Usage - returns Result<void, AppFailure>
final result = await deleteProductUseCase('product-123');
result.fold(
(_) => showSuccess('Product deleted'),
(failure) => showError(failure),
);
CompletableUseCase is useful when you only care about whether an operation succeeded or failed, without needing any returned data. Common use cases include:
- Delete operations
- Logout/sign out
- Clear cache
- Send analytics events
- Fire-and-forget notifications
SyncUseCase
For synchronous operations that don't require async processing (validation, calculations, transformations):
class ValidateEmailUseCase extends SyncUseCase<bool, String> {
@override
bool execute(String email) {
return RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$').hasMatch(email);
}
}
// Usage - returns Result<bool, AppFailure>
final validateEmail = ValidateEmailUseCase();
final result = validateEmail('user@example.com');
result.fold(
(isValid) => print('Email is valid: $isValid'),
(failure) => print('Validation failed: $failure'),
);
SyncUseCase is useful for operations that complete immediately without async I/O:
- Validation logic - Email, phone number, format checks
- Data transformations - Mapping, filtering, aggregations
- Calculations - Totals, averages, conversions
- Business rules - Eligibility, permissions, checks
Controller with State #
Controllers use StatefulController<T> with immutable state objects:
class ProductController extends Controller with StatefulController<ProductState> {
final ProductPresenter _presenter;
ProductController(this._presenter) : super();
@override
ProductState createInitialState() => const ProductState();
Future<void> getProductList() async {
updateState(viewState.copyWith(isGettingList: true));
final result = await _presenter.getProductList();
result.fold(
(list) => updateState(viewState.copyWith(
isGettingList: false,
productList: list,
)),
(failure) => updateState(viewState.copyWith(
isGettingList: false,
error: failure,
)),
);
}
Future<void> createProduct(Product product) async {
updateState(viewState.copyWith(isCreating: true));
final result = await _presenter.createProduct(product);
result.fold(
(created) => updateState(viewState.copyWith(
isCreating: false,
productList: [...viewState.productList, created],
)),
(failure) => updateState(viewState.copyWith(
isCreating: false,
error: failure,
)),
);
}
}
State #
Immutable state classes are auto-generated with the --state flag:
class ProductState {
final AppFailure? error;
final List<Product> productList;
final Product? product;
final bool isGetting;
final bool isCreating;
final bool isUpdating;
final bool isDeleting;
final bool isGettingList;
const ProductState({
this.error,
this.productList = const [],
this.product,
this.isGetting = false,
this.isCreating = false,
this.isUpdating = false,
this.isDeleting = false,
this.isGettingList = false,
});
ProductState copyWith({...}) => ...;
bool get isLoading => isGetting || isCreating || isUpdating || isDeleting || isGettingList;
bool get hasError => error != null;
}
CleanView #
Base class for views with automatic lifecycle management. Views are pure UI and delegate all business logic to the Controller:
class ProductView extends CleanView {
final ProductRepository productRepository;
const ProductView({super.key, required this.productRepository});
@override
State<ProductView> createState() => _ProductViewState(
ProductController(
ProductPresenter(productRepository: productRepository),
),
);
}
class _ProductViewState extends CleanViewState<ProductView, ProductController> {
_ProductViewState(super.controller);
@override
void onInitState() {
super.onInitState();
controller.getProductList();
}
@override
Widget get view {
return Scaffold(
key: globalKey, // Important: use globalKey on root widget
appBar: AppBar(title: const Text('Products')),
body: ControlledWidgetBuilder<ProductController>(
builder: (context, controller) {
if (controller.viewState.isLoading) {
return const CircularProgressIndicator();
}
return ListView.builder(
itemCount: controller.viewState.productList.length,
itemBuilder: (context, index) {
final product = controller.viewState.productList[index];
return ListTile(title: Text(product.name));
},
);
},
),
);
}
}
Dependency Injection Generation #
Zuraffa can automatically generate dependency injection setup using get_it:
# Generate DI files alongside your code
zfa generate Product --methods=get,getList,create --data --vpc --di
# Use mock datasource in DI (for development/testing)
zfa generate Product --methods=get,getList --data --mock --di --use-mock
# With caching enabled
zfa generate Product --methods=get,getList --data --cache --di
Generated DI Structure #
lib/src/di/
βββ index.dart # Main entry with setupDependencies()
βββ datasources/
β βββ index.dart # Auto-generated
β βββ product_remote_data_source_di.dart
βββ repositories/
β βββ index.dart # Auto-generated
β βββ product_repository_di.dart
βββ usecases/
β βββ index.dart # Auto-generated
β βββ get_product_usecase_di.dart
β βββ get_product_list_usecase_di.dart
βββ presenters/
β βββ index.dart # Auto-generated
β βββ product_presenter_di.dart
βββ controllers/
βββ index.dart # Auto-generated
β βββ product_controller_di.dart
Usage #
import 'package:get_it/get_it.dart';
import 'src/di/index.dart';
void main() {
final getIt = GetIt.instance;
setupDependencies(getIt);
runApp(MyApp());
}
// Access registered dependencies
final productRepository = getIt<ProductRepository>();
final productController = getIt<ProductController>();
Features #
- β One file per component: No merge conflicts
- β Auto-generated indexes: Directory scanning regenerates imports
- β
Cache support: Registers remote + local datasources when
--cacheused - β
Mock support: Use
--use-mockto register mock datasources - β Fail-safe: Regenerate anytime without manual merging
Cache Initialization (Hive) #
When using --cache with --di, Zuraffa automatically generates cache initialization files:
# Generate with cache and DI
zfa generate Product --methods=get,getList --data --cache --cache-policy=ttl --ttl=30 --di
Generated Cache Structure #
lib/src/cache/
βββ hive_registrar.dart # @GenerateAdapters for all entities
βββ hive_manual_additions.txt # Template for nested entities/enums
βββ product_cache.dart # Opens Product box
βββ timestamp_cache.dart # Opens timestamps box
βββ ttl_30_minutes_cache_policy.dart # Cache policy implementation
βββ index.dart # initAllCaches() + exports
Adding Nested Entities and Enums #
The generator creates hive_manual_additions.txt for entities that aren't directly cached but need adapters (nested entities, enums, etc.):
# Hive Manual Additions
# Format: import_path|EntityName
../domain/entities/enums/index.dart|ParserType
../domain/entities/enums/index.dart|HttpClientType
../domain/entities/range/range.dart|Range
../domain/entities/filter_parameter/filter_parameter.dart|FilterParameter
After adding entries, regenerate:
zfa generate Product --methods=get --data --cache --di --force
The registrar will include all manual additions:
@GenerateAdapters([
AdapterSpec<ParserType>(),
AdapterSpec<HttpClientType>(),
AdapterSpec<Range>(),
AdapterSpec<FilterParameter>(),
AdapterSpec<Product>()
])
Generated Files #
hive_registrar.dart - Automatic adapter registration:
@GenerateAdapters([AdapterSpec<Product>(), AdapterSpec<User>()])
part 'hive_registrar.g.dart';
extension HiveRegistrar on HiveInterface {
void registerAdapters() {
registerAdapter(ProductAdapter());
registerAdapter(UserAdapter());
}
}
Cache policy - Fully implemented with Hive:
CachePolicy createTtl30MinutesCachePolicy() {
final timestampBox = Hive.box<int>('cache_timestamps');
return TtlCachePolicy(
ttl: const Duration(minutes: 30),
getTimestamps: () async => Map<String, int>.from(timestampBox.toMap()),
setTimestamp: (key, timestamp) async => await timestampBox.put(key, timestamp),
removeTimestamp: (key) async => await timestampBox.delete(key),
clearAll: () async => await timestampBox.clear(),
);
}
Usage #
import 'package:hive_ce_flutter/hive_ce_flutter.dart';
import 'src/cache/index.dart';
import 'src/di/index.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await Hive.initFlutter();
await initAllCaches(); // Registers adapters + opens boxes
setupDependencies(GetIt.instance);
runApp(MyApp());
}
Workflow #
- Generate code with
--cacheand--di - Run
dart run build_runner build(generateshive_registrar.g.dart) - Call
initAllCaches()before DI setup
Features #
- β Automatic adapter registration: No manual Hive.registerAdapter() calls
- β
Separate policy files:
daily_cache_policy.dart,ttl_<N>_minutes_cache_policy.dart - β
Custom TTL: Use
--ttl=<minutes>for custom durations - β Type-safe: Abstract DataSource type allows easy mock/remote switching
Mock Data Generation #
Zuraffa can generate realistic mock data for your entities, perfect for testing, UI previews, and development:
# Generate mock data alongside other layers
zfa generate Product --methods=get,getList,create --vpc --mock
# Generate only mock data files
zfa generate Product --mock-data-only
Generated Mock Data #
Mock data files provide realistic test data with proper type safety:
// Generated: lib/src/data/mock/product_mock_data.dart
class ProductMockData {
static final List<Product> products = [
Product(
id: 'id 1',
name: 'name 1',
description: 'description 1',
price: 10.5,
category: 'category 1',
isActive: true,
createdAt: DateTime.now().subtract(Duration(days: 30)),
updatedAt: DateTime.now().subtract(Duration(days: 30)),
),
Product(
id: 'id 2',
name: 'name 2',
description: 'description 2',
price: 21.0,
category: 'category 2',
isActive: false,
createdAt: DateTime.now().subtract(Duration(days: 60)),
updatedAt: DateTime.now().subtract(Duration(days: 60)),
),
// ... more items
];
static Product get sampleProduct => products.first;
static List<Product> get sampleList => products;
static List<Product> get emptyList => [];
// Large dataset for performance testing
static List<Product> get largeProductList => List.generate(100,
(index) => _createProduct(index + 1000));
}
Features #
- β Realistic data: Type-appropriate values for all field types
- β Nested entities: Automatic detection and cross-references
- β
Complex types: Support for
List<T>,Map<K,V>, nullable types - β Enum handling: Smart imports only when needed
- β Large datasets: Generated methods for performance testing
- β Null safety: Proper handling of optional fields
Usage in Tests #
// Use in unit tests
test('should process product list', () {
final products = ProductMockData.sampleList;
final result = processProducts(products);
expect(result.length, equals(3));
});
// Use in widget tests
testWidgets('should display product', (tester) async {
await tester.pumpWidget(ProductView(
product: ProductMockData.sampleProduct,
));
expect(find.text('name 1'), findsOneWidget);
});
CLI Tool #
Zuraffa includes a powerful CLI tool (zfa) for generating boilerplate code.
Installation #
# Global activation
dart pub global activate zuraffa
# Or run directly
dart run zuraffa:zfa
Entity Commands (NEW!) #
Zuraffa now includes full Zorphy entity generation - create type-safe entities, enums, and manage data models:
# Create an entity with fields
zfa entity create -n User --field name:String --field email:String? --field age:int
# Create an enum
zfa entity enum -n Status --value active,inactive,pending
# Quick-create a simple entity
zfa entity new -n Product
# Add fields to existing entity
zfa entity add-field -n User --field phone:String?
# Create entity from JSON file
zfa entity from-json user_data.json
# List all entities
zfa entity list
# Build generated code
zfa build
zfa build --watch # Watch for changes
zfa build --clean # Clean and rebuild
Full Entity Generation Features:
- β Type-safe entities with null safety
- β JSON serialization (built-in)
- β Sealed classes for polymorphism
- β Multiple inheritance support
- β
Generic types (
List<T>,Map<K,V>) - β Nested entities with auto-imports
- β Enum integration
- β Self-referencing types (trees)
- β
compare
To,copyWith,patch` methods
π For complete entity generation documentation, see ENTITY_GUIDE.md
Generate Clean Architecture #
One command generates your entire feature:
# Generate everything at once - Domain, Data, and Presentation layers
zfa generate Product --methods=get,getList,create,update,delete --data --vpc --state
# Generate with mock data for testing and UI previews
zfa generate Product --methods=get,getList,create,update,delete --data --vpc --state --mock
# Generate only mock data files
zfa generate Product --mock-data-only
# Or generate incrementally:
# Generate UseCases + Repository interface
zfa generate Product --methods=get,getList,create,update,delete
# Add presentation layer (View, Presenter, Controller, State)
zfa generate Product --methods=get,getList,create,update,delete --vpc --state
# Add data layer (DataRepository + DataSource)
zfa generate Product --methods=get,getList,create,update,delete --data
# Use typed patches for updates (Zorphy support)
zfa generate Product --methods=update --zorphy
# Enable caching with dual datasources
zfa generate Config --methods=get,getList --data --cache --cache-policy=daily
# Preview what would be generated without writing files
zfa generate Product --methods=get,getList --dry-run
# Generate with unit tests for each UseCase
zfa generate Product --methods=get,create,update,delete --test
# Custom UseCase with repository
zfa generate SearchProduct --domain=search --repo=Product --params=Query --returns=List<Product>
# Custom UseCase with service (alternative to repository)
zfa generate SendEmail --domain=notifications --service=Email --params=EmailMessage --returns=void
# Orchestrator pattern (compose UseCases)
zfa generate ProcessCheckout --domain=checkout --usecases=ValidateCart,ProcessPayment --params=CheckoutRequest --returns=OrderResult
# Background UseCase for CPU-intensive operations (runs on isolate)
zfa generate CalculatePrimeNumbers --type=background --params=int --returns=int
Services vs Repositories
Zuraffa supports two patterns for dependency injection in custom UseCases:
Repositories (--repo) - Use for data access and entity operations
zfa generate GetProduct --domain=product --repo=Product --params=String --returns=Product
Services (--service) - Use for business logic, external APIs, or non-entity operations
# Generate a service interface
zfa generate SendEmail --domain=notifications --service=Email --params=EmailMessage --returns=void
# Generated service interface
abstract class EmailService {
Future<void> sendEmail(EmailMessage params);
}
When to use Services:
- External API integrations (payment gateways, email services, SMS)
- Business logic that doesn't involve entities
- Utility operations (file processing, calculations, transformations)
- Third-party SDK integrations (analytics, crash reporting, authentication)
When to use Repositories:
- CRUD operations on entities
- Data access (local or remote)
- Caching and offline support
- Data synchronization
GraphQL Generation
Generate GraphQL query/mutation/subscription files for your entities and UseCases with the --gql flag.
Entity-Based GraphQL Generation:
# Basic GraphQL with auto-generated queries
zfa generate Product --methods=get,getList --gql
# With custom return fields
zfa generate Product --methods=get,getList,create --gql --gql-returns="id,name,price,category,isActive,createdAt"
# With custom operation types
zfa generate Product --methods=watch,watchList --gql --gql-type=subscription
# Complete GraphQL setup
zfa generate Product --methods=get,getList,create,update,delete --data --gql --gql-returns="id,name,description,price,category,isActive,createdAt,updatedAt"
Generated files are placed in:
lib/src/data/data_sources/{entity}/graphql/
βββ get_product_query.dart
βββ get_product_list_query.dart
βββ create_product_mutation.dart
βββ update_product_mutation.dart
Custom UseCase with GraphQL:
# Custom UseCase with GraphQL query
zfa generate SearchProducts \
--service=Search \
--domain=products \
--params=SearchQuery \
--returns=List<Product> \
--gql \
--gql-type=query \
--gql-name=searchProducts \
--gql-input-type=SearchInput \
--gql-returns="id,name,price,category"
# Custom UseCase with GraphQL mutation
zfa generate UploadFile \
--service=Storage \
--domain=storage \
--params=FileData \
--returns=String \
--gql \
--gql-type=mutation \
--gql-name=uploadFile \
--gql-input-type=FileInput
# Custom UseCase with GraphQL subscription
zfa generate WatchUserLocation \
--service=Location \
--domain=realtime \
--params=UserId \
--returns=Location \
--gql \
--gql-type=subscription
GraphQL Options:
| Flag | Description |
|---|---|
--gql |
Enable GraphQL generation |
--gql-type |
Operation type: query, mutation, subscription (auto-detected for entity methods) |
--gql-returns |
Return fields as comma-separated string |
--gql-input-type |
Input type name for mutation/subscription |
--gql-input-name |
Input variable name (default: input) |
--gql-name |
Custom operation name (default: auto-generated) |
Auto-Detection for Entity Methods:
get,getListβ querycreate,update,deleteβ mutationwatch,watchListβ subscription
GraphQL Generation #
Zuraffa provides powerful GraphQL generation capabilities, creating type-safe GraphQL queries, mutations, and subscriptions directly from your entity and UseCase definitions.
Generated GraphQL Code #
When you use the --gql flag, Zuraffa generates GraphQL operation files with string constants that you can use directly with any GraphQL client (like graphql_flutter or ferry).
Example: Entity-Based Generation
zfa generate Product --methods=get,getList,create --gql --gql-returns="id,name,price,category,isActive,createdAt"
Generated File: lib/src/data/data_sources/product/graphql/get_product_query.dart
// Generated GraphQL query for GetProduct
const String getProductsQuery = r'''
query GetProduct($id: String!) {
getProduct(id: $id) {
id
name
price
category
isActive
createdAt
}
}''';
Generated File: lib/src/data/data_sources/product/graphql/create_product_mutation.dart
// Generated GraphQL mutation for CreateProduct
const String createProductMutation = r'''
mutation CreateProduct($input: CreateProductInput!) {
createProduct(input: $input) {
id
name
price
category
isActive
createdAt
}
}''';
Example: Custom UseCase with GraphQL
zfa generate SearchProducts \
--service=Search \
--domain=products \
--params=SearchQuery \
--returns=List<Product> \
--gql \
--gql-type=query \
--gql-name=searchProducts \
--gql-input-type=SearchInput \
--gql-returns="id,name,price,category,rating"
Generated File: lib/src/data/data_sources/products/graphql/search_products_query.dart
// Generated GraphQL query for SearchProducts
const String searchProductsQuery = r'''
query SearchProducts($input: SearchInput!) {
searchProducts(input: $input) {
id
name
price
category
rating
}
}''';
Using Generated GraphQL #
Here's how to use the generated GraphQL strings with a GraphQL client:
import 'package:graphql_flutter/graphql_flutter.dart';
import 'package:zuraffa_example/data/data_sources/product/graphql/get_product_query.dart';
import 'package:zuraffa_example/data/data_sources/product/graphql/create_product_mutation.dart';
class ProductGraphQLDataSource {
final GraphQLClient _client;
ProductGraphQLDataSource(this._client);
Future<Product> getProduct(String id) async {
final result = await _client.query(
QueryOptions(
document: gql(getProductQuery),
variables: {'id': id},
),
);
if (result.hasException) {
throw Exception('Failed to fetch product: ${result.exception}');
}
return Product.fromJson(result.data!['getProduct']);
}
Future<Product> createProduct(CreateProductInput input) async {
final result = await _client.mutate(
MutationOptions(
document: gql(createProductMutation),
variables: {'input': input.toJson()},
),
);
if (result.hasException) {
throw Exception('Failed to create product: ${result.exception}');
}
return Product.fromJson(result.data!['createProduct']);
}
}
Subscriptions #
Generate GraphQL subscriptions for real-time updates:
zfa generate Product --methods=watchList --gql --gql-type=subscription
Generated Subscription:
// Generated GraphQL subscription for WatchProductList
const String watchProductListSubscription = r'''
subscription WatchProductList {
watchProductList {
id
name
price
category
isActive
updatedAt
}
}''';
Usage:
ObservableQuery<List<Product>> watchProductList() {
return _client.subscribe(
SubscriptionOptions(
document: gql(watchProductListSubscription),
),
);
}
Custom Fields and Types #
You have full control over the generated GraphQL operations:
# Specify nested fields
zfa generate Order --methods=get --gql --gql-returns="id,createdAt,customer{id,name,email},items{id,quantity,price,product{id,name}}"
# Custom input types
zfa generate CreateOrder \
--service=Order \
--domain=orders \
--params=OrderInput \
--returns=Order \
--gql --gql-type=mutation \
--gql-input-type=OrderCreateInput
Coming Soon #
π§ Schema-First Generation (Coming Soon)
We're working on even more powerful GraphQL features:
- Auto-generate entities from GraphQL schema: Import your GraphQL schema and Zuraffa will automatically create all entity definitions
- Auto-generate UseCases from queries/mutations: Parse your
.graphqlfiles and generate corresponding UseCases with proper types - Type safety from schema to code: Ensure complete type alignment between your GraphQL server and Flutter app
- Introspection support: Fetch live schema from GraphQL server to generate up-to-date types
- Federation support: Generate code for federated GraphQL schemas
These features will make building GraphQL-powered Flutter apps even more seamless, eliminating manual type mapping and reducing boilerplate to near-zero.
Quick Reference #
| Flag | Description | Example |
|---|---|---|
--gql |
Enable GraphQL generation | --gql |
--gql-type |
Operation type: query, mutation, subscription | --gql-type=query |
--gql-returns |
Return fields (comma-separated) | --gql-returns="id,name,price" |
--gql-input-type |
Input type name | --gql-input-type=ProductInput |
--gql-input-name |
Input variable name | --gql-input-name=input |
--gql-name |
Custom operation name | --gql-name=getProductById |
Custom UseCase Types
The --type flag supports three variants for custom UseCases:
| Type | Description | Use When |
|---|---|---|
custom (default) |
Standard UseCase with repository/service dependencies | CRUD operations, business logic |
background |
Runs on a separate isolate | CPU-intensive work (calculations, image processing) |
stream |
Emits multiple values over time | Real-time data, WebSocket, Firebase listeners |
Defining Parameter and Return Types
Use --params and --returns to specify custom types for your UseCase:
# Define custom parameter and return types
zfa generate CalculatePrimeNumbers --type=background --params=int --returns=int
# Orchestrator with multiple UseCases
zfa generate ProcessCheckout --domain=checkout --usecases=ValidateCart,ProcessPayment --params=CheckoutRequest --returns=OrderConfirmation
| Flag | Description | Example |
|---|---|---|
--params |
Input parameter type for the UseCase | --params=int, --params=ProductFilter |
--returns |
Return type from the UseCase | --returns=bool, --returns=List<Product> |
Available Methods #
| Method | UseCase Type | Description |
|---|---|---|
get |
UseCase | Get single entity by ID |
getList |
UseCase | Get all entities |
create |
UseCase | Create new entity |
update |
UseCase | Update existing entity |
delete |
CompletableUseCase | Delete entity by ID |
watch |
StreamUseCase | Watch single entity |
watchList |
StreamUseCase | Watch all entities |
CLI Flags #
| Flag | Description |
|---|---|
--data |
Generate DataRepository and DataSource (always includes remote datasource) |
--vpc |
Generate View, Presenter, and Controller |
--vpcs |
Generate View, Presenter, Controller, and State |
--pc |
Generate Presenter and Controller only (preserve View) |
--pcs |
Generate Presenter, Controller, and State (preserve View) |
--repo |
Repository to inject (for custom UseCases) |
--service |
Service interface to inject (alternative to --repo) |
--domain |
Domain folder (required for custom UseCases) |
--append |
Append to existing repository/datasources |
--usecases |
Orchestrator: compose UseCases (comma-separated) |
--variants |
Polymorphic: generate variants (comma-separated) |
--state |
Generate immutable State class |
--mock |
Generate mock data files alongside other layers |
--mock-data-only |
Generate only mock data files (no other layers) |
--use-mock |
Use mock datasource in DI (default: remote datasource) |
--di |
Generate dependency injection files (get_it) |
--zorphy |
Use typed Patch objects for updates |
--cache |
Enable caching with dual datasources (remote + local) |
--cache-policy |
Cache expiration: daily, restart, ttl (default: daily) |
--cache-storage |
Local storage hint: hive, sqlite, shared_preferences (default: hive) |
--ttl |
TTL duration in minutes (default: 1440 = 24 hours) |
--subfolder |
Organize under a subfolder (e.g., --subfolder=auth) |
--init |
Add initialize method & isInitialized stream to repos |
--force |
Overwrite existing files |
--dry-run |
Preview what would be generated without writing files |
--test |
Generate unit tests for each UseCase |
--format=json |
Output JSON for AI/IDE integration |
--gql |
Generate GraphQL query/mutation/subscription files |
--gql-type |
GraphQL operation type: query, mutation, subscription |
--gql-returns |
GraphQL return fields (comma-separated) |
--gql-input-type |
GraphQL input type name |
--gql-input-name |
GraphQL input variable name (default: input) |
--gql-name |
Custom GraphQL operation name |
AI/JSON Integration #
# JSON output for parsing
zfa generate Product --methods=get,getList --format=json
# Read from stdin
echo '{"name":"Product","methods":["get","getList"]}' | zfa generate Product --from-stdin
# Get JSON schema for validation
zfa schema
# Dry run (preview without writing)
zfa generate Product --methods=get --dry-run --format=json
For complete CLI documentation, see CLI_GUIDE.md.
MCP Server #
Zuraffa includes an MCP (Model Context Protocol) server for seamless integration with AI-powered development environments like Claude Desktop, Cursor, and VS Code.
Installation #
Option 1: From pub.dev (Recommended)
dart pub global activate zuraffa
# MCP server is immediately available: zuraffa_mcp_server
Option 2: Pre-compiled Binary (Fastest)
Download from GitHub Releases:
- macOS ARM64 / x64
- Linux x64
- Windows x64
# macOS/Linux
chmod +x zuraffa_mcp_server-macos-arm64
sudo mv zuraffa_mcp_server-macos-arm64 /usr/local/bin/zuraffa_mcp_server
Option 3: Compile from Source
dart compile exe bin/zuraffa_mcp_server.dart -o zuraffa_mcp_server
MCP Tools #
zuraffa_generate- Generate Clean Architecture codezuraffa_schema- Get JSON schema for config validationzuraffa_validate- Validate a generation config
For complete MCP documentation, see MCP_SERVER.md.
Project Structure #
Recommended folder structure for Clean Architecture (auto-generated by zfa):
lib/
βββ main.dart
βββ src/
βββ core/ # Shared utilities
β βββ error/ # Custom failures if needed
β βββ network/ # HTTP client, interceptors
β βββ utils/ # Helpers, extensions
β
βββ data/ # Data layer
β βββ data_sources/ # Remote and local data sources
β β βββ product/
β β βββ product_data_source.dart
β βββ repositories/ # Repository implementations
β βββ data_product_repository.dart
β
βββ domain/ # Domain layer (pure Dart)
β βββ entities/ # Business objects
β β βββ product/
β β βββ product.dart
β βββ repositories/ # Repository interfaces
β β βββ product_repository.dart
β βββ services/ # Service interfaces (alternative to repositories)
β β βββ email_service.dart
β βββ usecases/ # Business logic
β βββ product/
β βββ get_product_usecase.dart
β βββ create_product_usecase.dart
β βββ ...
β
βββ presentation/ # Presentation layer
βββ pages/ # Full-screen views
βββ product/
βββ product_view.dart
βββ product_presenter.dart
βββ product_controller.dart
βββ product_state.dart
All of this is generated with a single command:
zfa generate Product --methods=get,getList,create,update,delete --data --vpc --state
Advanced Features #
CancelToken #
Cooperative cancellation for long-running operations:
// Create a token
final cancelToken = CancelToken();
// Use with a use case
final result = await getProductUseCase(productId, cancelToken: cancelToken);
// Cancel when needed
cancelToken.cancel('Product page closed');
// Create with timeout
final timeoutToken = CancelToken.timeout(const Duration(seconds: 30));
// In Controllers, use createCancelToken() for automatic cleanup
class MyController extends Controller {
Future<void> loadData() async {
// Token automatically cancelled when controller disposes
final result = await execute(myUseCase, params);
}
}
ControlledWidgetSelector #
For fine-grained rebuilds when only specific values change:
// Only rebuilds when product.name changes
ControlledWidgetSelector<ProductController, String?>(
selector: (controller) => controller.viewState.product?.name,
builder: (context, productName) {
return Text(productName ?? 'Unknown');
},
)
Global Configuration #
void main() {
// Enable debug logging
Zuraffa.enableLogging();
runApp(MyApp());
}
// Access controllers from child widgets
class MyWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
final controller = Zuraffa.getController<MyController>(context);
return ElevatedButton(
onPressed: () => controller.doSomething(),
child: Text('Action'),
);
}
}
Example #
See the example directory for a complete working application demonstrating:
- β UseCase for CRUD operations
- β StreamUseCase for real-time updates
- β BackgroundUseCase for CPU-intensive calculations
- β Controller with immutable state
- β CleanView with ControlledWidgetBuilder
- β CancelToken for cancellation
- β Error handling with AppFailure
Run the example:
cd example
flutter pub get
flutter run
Documentation #
- CLI Guide - Complete CLI documentation
- Caching Guide - Dual datasource caching pattern
- MCP Server - MCP server setup and usage
- AGENTS.md - Guide for AI coding agents
- Contributing - How to contribute
- Code of Conduct - Community guidelines
License #
MIT License - see LICENSE for details.
Authors #
- Ahmet TOK - GitHub
Made with β‘οΈ for the Flutter community