â¤ī¸ Support Flutter Command Pattern

If this plugin helps you create cleaner and more predictable Flutter applications, please consider leaving a 👍 here and a ⭐ on GitHub — it really helps with the growth and visibility of the project.

You can also support the development by buying me a coffee ☕👇

Buy Me a Coffee


Flutter Command Pattern

pub package Build Status codecov License: MIT

A lightweight yet powerful Command Pattern implementation for Flutter, focused on simplicity, explicit state, and zero external dependencies.

Designed for applications using MVVM and Clean Architecture, without relying on streams, Rx, or reactive libraries.


Features

  • ✨ Type-safe Commands – Encapsulate async actions with explicit execution state
  • 🔄 Pipeline System – Intercept command execution for logging, analytics, caching, etc.
  • 👀 Global & Lifecycle-safe Observers – React to command lifecycle events across the app without memory leaks
  • đŸŽ¯ Explicit State – Loading, success, and failure handled in a predictable way
  • đŸ“Ļ Zero dependencies – Pure Flutter implementation

Why Flutter Command Pattern?

This package is built for developers who want:

  • Clear and predictable async flows
  • No dependency on streams, Rx, or reactive abstractions
  • Simple commands that integrate naturally with MVVM and Clean Architecture
  • Centralized interception via pipelines (without magic)
  • UI code free from business logic

If you prefer explicitness over abstraction, this package is for you.


Installation

dependencies:
  flutter_command_pattern: ^1.1.1

Quick Start (MVVM-oriented)

ViewModel with Commands

In MVVM, commands usually live inside the ViewModel and represent user intents or use cases.

class LoginViewModel {
  final AuthService authService;

  late final Command loginCommand;

  LoginViewModel(this.authService) {
    loginCommand = Command(() async {
      await authService.login(email, password);
    });
  }

  String email = '';
  String password = '';
}

Executing the Command from the UI

ElevatedButton(
  onPressed: () => viewModel.loginCommand.execute(),
  child: const Text('Login'),
);

Observing Command State in the UI

@override
void initState() {
  super.initState();

  viewModel.loginCommand.observe(
    context,
    onLoading: (_) => showLoader(context),
    onSuccess: (_) => navigateToHome(context),
    onFailure: (_, error) => showError(context, error),
  );
}

This keeps widgets dumb and moves all business logic into the ViewModel.


Command with Parameters (Use Case style)

Commands can also represent parameterized use cases:

class FetchUserViewModel {
  final UserApi api;

  late final User user;

  late final CommandWithParams<String> fetchUserCommand;

  FetchUserViewModel(this.api) {
    fetchUserCommand = CommandWithParams((userId) async {
      user = api.fetchUser(userId);
    });
  }
}
viewModel.fetchUserCommand.execute('user-123');

Global Pipelines

Pipelines allow cross-cutting concerns without polluting ViewModels.

void main() {
  CommandPipelineRegistry.addPipeline((context, next) async {
    debugPrint('Command started: ${context.command.runtimeType}');
    await next();
    debugPrint('Command finished with state: ${context.state.runtimeType}');
  });

  runApp(const MyApp());
}

Common use cases:

  • Logging
  • Performance monitoring
  • Analytics
  • Authorization checks

Global Observers

Observers react to command lifecycle events globally.

CommandObserverRegistry.addObserver((context) {
  if (context.state is CommandFailure) {
    analytics.logError(
      command: context.command,
      error: (context.state as CommandFailure).error,
    );
  }
});

â„šī¸ observe is lifecycle-safe.
Listeners are automatically removed when the widget is disposed, so no manual cleanup is required — even on Flutter Web.


Error Mapping (Custom Error Handling)

Map custom exception types to standardized CommandError objects for consistent error handling across your app.

Register Custom Error Mappers

void main() {
  // Register mappers for your custom exceptions
  CommandErrorMapperRegistry.register<NetworkException>(
    (error) => CommandError(
      code: 'NETWORK_ERROR',
      message: 'Failed to connect to server',
      initialError: error,
    ),
  );

  CommandErrorMapperRegistry.register<ValidationException>(
    (error) => CommandError(
      code: 'VALIDATION_${error.field}',
      message: 'Invalid ${error.field}: ${error.message}',
      initialError: error,
    ),
  );

  runApp(const MyApp());
}

Handling Mapped Errors in the UI

ListenableBuilder(
  listenable: command,
  builder: (context, _) {
    if (command.hasError) {
      final error = command.error;
      
      return switch (error?.code) {
        'NETWORK_ERROR' => const Text('Check your internet connection'),
        'VALIDATION_email' => const Text('Invalid email format'),
        _ => Text('Error: ${error?.message}'),
      };
    }
    return const SizedBox.shrink();
  },
)

Automatic Fallback (No Mapper Registered)

If no mapper is registered for an exception type, a default CommandError is created automatically:

// When an unmapped exception is thrown:
throw SomeUnregistredException('Something went wrong');

// Results in:
CommandError(
  code: null,  // No code when not mapped
  message: 'SomeUnregistredException: Something went wrong',
  initialError: SomeUnregistredException instance,
)

This ensures your app never crashes from unhandled errors — they're always wrapped in CommandError.

💡 Tip: Use the error's initialError property to access the original exception for detailed debugging or analytics.


Quality

  • ✅ Pub.dev score: 160 / 160
  • ✅ Fully null-safe
  • ✅ Zero external dependencies
  • ✅ High test coverage

Example

See the example for a complete implementation.


License

MIT License - see LICENSE file for details.

Libraries

flutter_command_pattern
A powerful command pattern implementation for Flutter.