zuraffa 1.12.1 copy "zuraffa: ^1.12.1" to clipboard
zuraffa: ^1.12.1 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 #

Pub Version License: MIT

A comprehensive Clean Architecture framework for Flutter applications with Result-based error handling, type-safe failures, and minimal boilerplate.

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 #

  • βœ… Result Type: Type-safe error handling with Result<T, AppFailure>
  • βœ… Sealed Failures: Exhaustive pattern matching for error cases
  • βœ… UseCase Pattern: Single-shot, streaming, and background operations
  • βœ… Controller: Simple state management with automatic cleanup
  • βœ… CLI Tool: Generate boilerplate code with zfa command
  • βœ… MCP Server: AI/IDE integration via Model Context Protocol
  • βœ… Cancellation: Cooperative cancellation with CancelToken
  • βœ… Fine-grained Rebuilds: Optimize performance with selective widget updates
  • βœ… Caching: Built-in dual datasource pattern with flexible cache policies

Installation #

Add this to your package's pubspec.yaml file:

dependencies:
  zuraffa: ^1.12.0

Then run:

flutter pub get

Quick Start #

The fastest way to try Zuraffa is to create a sample entity first:

# Activate the CLI
dart pub global activate zuraffa

# Create a sample Product entity to test with
zfa initialize

# Or create a different entity
zfa initialize --entity=User

# Generate complete Clean Architecture around your entity
zfa generate Product --methods=get,getList,create,update,delete --repository --data --vpc --state

2. Generate Code with the CLI #

One command generates your entire feature:


# Generate a complete feature with one line of code
# This creates 14 files: UseCases, Repository, DataSource, Presenter, Controller, State, and View
zfa generate Product --methods=get,watch,create,update,delete,getList,watchList --repository --data --vpc --state --test

# Or use the shorter alias
dart run zuraffa:zfa generate Product --methods=get,getList --repository --vpc --state

That's it! One command generates:

  • βœ… Domain layer (UseCases + Repository interface)
  • βœ… Data layer (DataRepository + DataSource)
  • βœ… Presentation layer (View, Presenter, Controller, State)

2. 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 #

βœ… Generated 21 files for Product

  ⟳ lib/src/domain/repositories/product_repository.dart
  ⟳ lib/src/domain/usecases/product/get_product_usecase.dart
  ⟳ lib/src/domain/usecases/product/watch_product_usecase.dart
  ⟳ lib/src/domain/usecases/product/create_product_usecase.dart
  ⟳ lib/src/domain/usecases/product/update_product_usecase.dart
  ⟳ lib/src/domain/usecases/product/delete_product_usecase.dart
  ⟳ lib/src/domain/usecases/product/get_product_list_usecase.dart
  ⟳ lib/src/domain/usecases/product/watch_product_list_usecase.dart
  ⟳ lib/src/presentation/pages/product/product_presenter.dart
  ⟳ lib/src/presentation/pages/product/product_controller.dart
  ⟳ lib/src/presentation/pages/product/product_view.dart
  ⟳ lib/src/presentation/pages/product/product_state.dart
  ⟳ lib/src/data/data_sources/product/product_data_source.dart
  ⟳ lib/src/data/repositories/data_product_repository.dart
  βœ“ test/domain/usecases/product/get_product_usecase_test.dart
  βœ“ test/domain/usecases/product/watch_product_usecase_test.dart
  βœ“ test/domain/usecases/product/create_product_usecase_test.dart
  βœ“ test/domain/usecases/product/update_product_usecase_test.dart
  βœ“ test/domain/usecases/product/delete_product_usecase_test.dart
  βœ“ test/domain/usecases/product/get_product_list_usecase_test.dart
  βœ“ test/domain/usecases/product/watch_product_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 Morphy (--morphy)

If you use Morphy or similar tools, you can use typed Patch objects for full type safety.

zfa generate Product --methods=update --morphy
// Generated with --morphy
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

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));
            },
          );
        },
      ),
    );
  }
}

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 --repository --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

Initialize Command #

The quickest way to get started is with the initialize command:

# Create a sample Product entity with common fields
zfa initialize

# Create a different entity
zfa initialize --entity=User

# Preview without writing files
zfa initialize --dry-run

# Specify custom output directory
zfa initialize --entity=Order --output=lib/src

The initialize command creates a sample entity with realistic fields:

  • id (String) - Unique identifier
  • name (String) - Display name
  • description (String) - Detailed description
  • price (double) - Numeric value
  • category (String) - Classification
  • isActive (bool) - Status flag
  • createdAt (DateTime) - Creation timestamp
  • updatedAt (DateTime?) - Optional update timestamp

This gives you a complete entity to immediately test Zuraffa's code generation capabilities.

Basic Usage #

One command generates your entire feature:

# Generate everything at once - Domain, Data, and Presentation layers
zfa generate Product --methods=get,getList,create,update,delete --repository --data --vpc --state

# Generate with mock data for testing and UI previews
zfa generate Product --methods=get,getList,create,update,delete --repository --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 --repository

# Add presentation layer (View, Presenter, Controller, State)
zfa generate Product --methods=get,getList,create,update,delete --repository --vpc --state

# Add data layer (DataRepository + DataSource)
zfa generate Product --methods=get,getList,create,update,delete --repository --data

# Use typed patches for updates (Morphy support)
zfa generate Product --methods=update --morphy

# Enable caching with dual datasources
zfa generate Config --methods=get,getList --repository --data --cache --cache-policy=daily

# Preview what would be generated without writing files
zfa generate Product --methods=get,getList --repository --dry-run

# Generate with unit tests for each UseCase
zfa generate Product --methods=get,create,update,delete --repository --test

# Generate in a subfolder (e.g., for auth-related entities)
zfa generate Session --methods=get,create --repository --subfolder=auth

# Custom UseCase with multiple repositories
zfa generate PublishProduct --repos=ProductRepository,CategoryRepository --params=PublishProductRequest --returns=PublishedProduct

# Background UseCase for CPU-intensive operations (runs on isolate)
zfa generate CalculatePrimeNumbers --type=background --params=int --returns=int

Custom UseCase Types

The --type flag supports three variants for custom UseCases:

Type Description Use When
custom (default) Standard UseCase with repository 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

# Complex types with multiple repositories
zfa generate ProcessCheckout --repos=CartRepository,PaymentRepository --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
--repository Generate repository interface
--data Generate DataRepository and DataSource
--vpc Generate View, Presenter, and Controller
--state Generate immutable State class
--mock Generate mock data files alongside other layers
--mock-data-only Generate only mock data files (no other layers)
--morphy 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
--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

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.

Running the MCP Server #

# Compile for faster startup
dart compile exe bin/zuraffa_mcp_server.dart -o zuraffa_mcp_server

# Run the server
./zuraffa_mcp_server

MCP Tools #

  • zuraffa_generate - Generate Clean Architecture code
  • zuraffa_schema - Get JSON schema for config validation
  • zuraffa_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
    β”‚   └── 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 --repository --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 #


Made with ⚑️ for the Flutter community

2
likes
0
points
1.89k
downloads

Publisher

verified publisherzuzu.dev

Weekly Downloads

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.

Homepage
Repository (GitHub)
View/report issues

Topics

#clean-architecture #state-management #dependency-injection #architecture #mvp

Documentation

Documentation

License

unknown (license)

Dependencies

args, flutter, logging, meta, path, provider, responsive_builder

More

Packages that depend on zuraffa