smart_executer 1.3.0+2 copy "smart_executer: ^1.3.0+2" to clipboard
smart_executer: ^1.3.0+2 copied to clipboard

A powerful Flutter package for executing async operations with built-in error handling, loading dialogs, retry logic, and Result pattern support.

Smart Executer #

pub package likes popularity License: MIT points Platform Live Demo

A powerful Flutter package for executing async operations with built-in error handling, loading dialogs, retry logic, and Result pattern support.

Features #

  • Loading Dialogs - Customizable loading dialogs during operations
  • Error Handling - Comprehensive error handling with specific callbacks for each error type
  • Result Pattern - Type-safe success/failure handling using sealed classes
  • Exception Metadata - Attach debugging info to exceptions for better error tracking
  • Retry Logic - Automatic retry with configurable attempts and delays
  • Connection Checking - Optional network connectivity verification before requests
  • Session Management - Built-in session expiration (401) handling
  • Stream Support - First-class support for stream-based operations with progress tracking
  • Status Cards - Ready-to-use cards for error, success, warning, info, and empty states
  • Customizable UI - Fully customizable dialogs, snack bars, and error messages
  • Global Configuration - Configure once, use everywhere

Installation #

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

dependencies:
  smart_executer: ^1.3.0

Then run:

flutter pub get

Quick Start #

1. Initialize (Optional) #

Configure SmartExecuter globally in your main.dart:

void main() {
  SmartExecuterConfig.initialize(
    enableLogging: true,
    defaultErrorMessage: 'Something went wrong. Please try again.',
    noConnectionMessage: 'No internet connection',
    maxRetries: 3,
    retryDelay: const Duration(seconds: 1),
  );
  runApp(const MyApp());
}

2. Basic Usage #

// Execute with loading dialog
final user = await SmartExecuter.run(
  () => apiService.getUser(id),
  context: context,
);

if (user != null) {
  print('User: ${user.name}');
}

3. Background Execution #

// Execute without loading dialog
final data = await SmartExecuter.inBackground(
  () => apiService.refreshCache(),
  context: context,
);

Usage Examples #

Using Result Pattern #

The Result pattern provides a type-safe way to handle success and failure cases:

final result = await SmartExecuter.execute(
  () => apiService.getUser(id),
);

// Using switch expression
switch (result) {
  case Success(:final data):
    print('User: ${data.name}');
  case Failure(:final exception):
    print('Error: ${exception.message}');
}

// Using fold
final userName = result.fold(
  onSuccess: (user) => user.name,
  onFailure: (exception) => 'Unknown',
);

// Using getOrElse
final user = result.getOrElse(User.empty());

// Chaining with map
final userEmail = result
    .map((user) => user.email)
    .getOrElse('no-email@example.com');

With Callbacks #

await SmartExecuter.run(
  () => apiService.createUser(userData),
  context: context,
  onSuccess: (user) async {
    ScaffoldMessenger.of(context).showSnackBar(
      SnackBar(content: Text('User ${user.name} created!')),
    );
    Navigator.of(context).pop(user);
  },
  onError: (exception) async {
    print('Failed to create user: ${exception.message}');
  },
  onConnectionError: () async {
    print('No internet connection');
  },
  onSessionExpired: () async {
    Navigator.of(context).pushReplacementNamed('/login');
  },
);

With Exception Metadata (Debugging) #

Attach metadata to exceptions for better debugging and error tracking:

await SmartExecuter.run(
  () => apiService.createOrder(orderData),
  context: context,
  options: ExecuterOptions(
    operationName: 'createOrder',
    metadata: {
      'userId': currentUser.id,
      'orderId': order.id,
      'screen': 'checkout',
      'cartItems': cart.itemCount,
    },
  ),
  onError: (exception) async {
    // Access metadata in the exception
    print('Operation: ${exception.metadata.operationName}');
    print('Endpoint: ${exception.metadata.endpoint}');
    print('Method: ${exception.metadata.requestMethod}');
    print('Timestamp: ${exception.metadata.timestamp}');
    print('Extra: ${exception.metadata.extra}');

    // Send to analytics/crash reporting
    analytics.logError(
      exception.metadata.operationName ?? 'unknown',
      exception.metadata.toMap(),
    );
  },
);

Using with execute():

final result = await SmartExecuter.execute(
  () => apiService.fetchUser(id),
  operationName: 'fetchUser',
  metadata: {'userId': id, 'source': 'profile_page'},
);

result.onFailure((exception) {
  // Full debugging context available
  crashlytics.recordError(
    exception,
    reason: exception.metadata.operationName,
    information: [
      'endpoint: ${exception.metadata.endpoint}',
      'method: ${exception.metadata.requestMethod}',
      ...exception.metadata.toMap().entries.map((e) => '${e.key}: ${e.value}'),
    ],
  );
});

With Retry Logic #

final data = await SmartExecuter.run(
  () => apiService.fetchData(),
  context: context,
  options: ExecuterOptions(
    maxRetries: 3,
    retryDelay: const Duration(seconds: 2),
    checkConnection: true,
  ),
);

Stream Operations with Progress #

await SmartExecuter.runStream(
  () => uploadService.uploadWithProgress(file),
  context: context,
  options: ExecuterOptions(
    operationName: 'uploadFile',
    metadata: {'fileName': file.name, 'fileSize': file.size},
  ),
  listener: (progress) {
    print('Upload progress: ${progress.percentage}%');
  },
  waitingBuilder: (context, progress) {
    return SmartProgressDialog(
      progress: progress.percentage / 100,
      message: 'Uploading file...',
    );
  },
  onSuccess: (result) async {
    print('Upload complete: ${result.url}');
  },
);

Custom Loading Dialog #

await SmartExecuter.run(
  () => apiService.fetchData(),
  context: context,
  options: ExecuterOptions(
    loadingWidget: const SmartLoadingDialog(
      message: 'Loading data...',
      indicatorColor: Colors.blue,
    ),
  ),
);

Configuration #

Global Configuration #

Configure SmartExecuter once at app startup:

SmartExecuterConfig.initialize(
  // Custom loading dialog
  loadingDialogBuilder: (context) => const MyCustomLoadingDialog(),

  // Custom error snack bar
  errorSnackBarBuilder: (context, exception) => MyErrorSnackBar(
    message: exception.message,
  ),

  // Global error handler
  globalErrorHandler: (exception) async {
    // Log all errors with metadata
    logger.error(
      'Error in ${exception.metadata.operationName}',
      error: exception,
      extra: exception.metadata.toMap(),
    );
  },

  // Session expiration handler
  onSessionExpired: () async {
    await authService.logout();
    navigatorKey.currentState?.pushReplacementNamed('/login');
  },

  // Messages
  defaultErrorMessage: 'An error occurred',
  noConnectionMessage: 'No internet connection',
  sessionExpiredMessage: 'Your session has expired',
  sessionExpiredTitle: 'Session Expired',

  // Retry configuration
  maxRetries: 3,
  retryDelay: const Duration(seconds: 1),

  // Connection checking
  checkConnectionByDefault: false,

  // Logging
  enableLogging: kDebugMode,
);

Per-Operation Options #

Override global configuration for specific operations:

final options = ExecuterOptions(
  showLoadingDialog: true,
  checkConnection: true,
  maxRetries: 5,
  retryDelay: const Duration(seconds: 2),
  timeout: const Duration(seconds: 30),
  barrierDismissible: false,
  barrierColor: Colors.black54,
  loadingWidget: const CircularProgressIndicator(),
  operationName: 'longRunningOperation',
  metadata: {'priority': 'high'},
);

await SmartExecuter.run(
  () => apiService.longRunningOperation(),
  context: context,
  options: options,
);

Exception Metadata #

All exceptions include metadata for debugging:

ExceptionMetadata Fields #

Field Type Description
operationName String? Name of the operation (e.g., 'fetchUser')
endpoint String? API endpoint (auto-extracted from Dio)
requestMethod String? HTTP method (auto-extracted from Dio)
userId String? User identifier
sessionId String? Session identifier
timestamp DateTime? When the error occurred
extra Map<String, dynamic>? Custom data

Using Metadata #

// Access metadata from exception
exception.metadata.operationName  // 'createOrder'
exception.metadata.endpoint       // '/api/orders'
exception.metadata.requestMethod  // 'POST'
exception.metadata.timestamp      // 2024-01-01 12:00:00
exception.metadata.extra          // {'userId': '123', 'orderId': '456'}

// Check if metadata has data
if (exception.metadata.hasData) {
  print(exception.metadata);
}

// Convert to Map for logging/serialization
final map = exception.metadata.toMap();
// {operationName: 'createOrder', endpoint: '/api/orders', ...}

// Attach metadata to existing exception
final enrichedException = exception.withMetadata(
  ExceptionMetadata(
    operationName: 'retryOperation',
    extra: {'attempt': 2},
  ),
);

Exception Types #

SmartExecuter provides specific exception types for different error scenarios:

Exception Description
ConnectionException No internet connection
ConnectionTimeoutException Connection timeout
SendTimeoutException Request send timeout
ReceiveTimeoutException Response receive timeout
CancelledException Request was cancelled
ResponseException Server error response
SessionExpiredException 401 Unauthorized
UnknownException Unknown error
final result = await SmartExecuter.execute(() => apiService.getData());

result.onFailure((exception) {
  // Access metadata in any exception type
  print('Failed: ${exception.metadata.operationName}');

  switch (exception) {
    case ConnectionException():
      showOfflineMessage();
    case SessionExpiredException():
      redirectToLogin();
    case ResponseException(:final statusCode):
      if (statusCode == 404) showNotFoundMessage();
    default:
      showGenericError();
  }
});

Widgets #

SmartLoadingDialog #

A customizable loading dialog:

const SmartLoadingDialog(
  message: 'Please wait...',
  indicatorColor: Colors.blue,
  indicatorSize: 48.0,
  backgroundColor: Colors.white,
  elevation: 8.0,
)

SmartProgressDialog #

A loading dialog with progress indicator:

SmartProgressDialog(
  progress: 0.75,
  message: 'Uploading...',
  showPercentage: true,
  progressColor: Colors.green,
)

SmartErrorSnackBar #

An error snack bar with automatic styling:

SmartErrorSnackBar(
  exception: exception,
  customMessage: 'Custom error message',
  duration: const Duration(seconds: 4),
)

SmartSuccessSnackBar #

A success snack bar:

SmartSuccessSnackBar(
  message: 'Operation completed successfully!',
  duration: const Duration(seconds: 3),
)

Helper Methods #

// Show error
SmartSnackBars.showError(context, exception);

// Show success
SmartSnackBars.showSuccess(context, 'Success!');

// Show custom
SmartSnackBars.show(
  context,
  'Custom message',
  backgroundColor: Colors.orange,
  icon: Icons.warning,
);

Status Cards #

Ready-to-use cards for displaying different states in your UI.

Basic Cards #

// Error card
SmartErrorCard(
  title: 'Something went wrong',
  message: 'Please try again later',
  action: 'Retry',
  onActionPressed: () => fetchData(),
)

// Success card
SmartSuccessCard(
  title: 'Success!',
  message: 'Your order has been placed',
  action: 'Continue',
  onActionPressed: () => navigateHome(),
)

// Warning card
SmartWarningCard(
  title: 'Warning',
  message: 'Your session will expire soon',
  action: 'Extend Session',
  onActionPressed: () => extendSession(),
)

// Info card
SmartInfoCard(
  title: 'Did you know?',
  message: 'Swipe to dismiss notifications',
  action: 'Got it',
  onActionPressed: () => dismiss(),
)

// Empty state card
SmartEmptyCard(
  title: 'No items yet',
  message: 'Add your first item to get started',
  action: 'Add Item',
  onActionPressed: () => addItem(),
)

// Loading card
SmartLoadingCard(
  title: 'Loading...',
  message: 'Please wait while we fetch your data',
)

Pre-configured Cards #

Specialized cards with pre-configured titles and messages:

// Offline card
SmartOfflineCard(
  onActionPressed: () => retry(),
)

// Session expired card
SmartSessionExpiredCard(
  onActionPressed: () => navigateToLogin(),
)

// Timeout card
SmartTimeoutCard(
  onActionPressed: () => retry(),
)

// Server error card
SmartServerErrorCard(
  onActionPressed: () => retry(),
  onSecondaryActionPressed: () => contactSupport(),
)

// Maintenance card
SmartMaintenanceCard()

// Permission denied card
SmartPermissionDeniedCard(
  permission: 'Camera',
  onActionPressed: () => requestPermission(),
  onSecondaryActionPressed: () => openSettings(),
)

// Not found card
SmartNotFoundCard(
  itemName: 'Product',
  onActionPressed: () => goBack(),
)

From Exception #

Create error cards directly from exceptions:

SmartErrorCard.fromException(
  exception,
  action: 'Retry',
  onActionPressed: () => retry(),
)

Available Status Cards #

Card Description
SmartErrorCard General error state
SmartSuccessCard Success state
SmartWarningCard Warning state
SmartInfoCard Information state
SmartEmptyCard Empty/no data state
SmartLoadingCard Loading state
SmartOfflineCard No internet connection
SmartSessionExpiredCard Session expired
SmartTimeoutCard Request timeout
SmartServerErrorCard Server error
SmartMaintenanceCard Under maintenance
SmartPermissionDeniedCard Permission required
SmartNotFoundCard Resource not found

Custom Widgets #

Status cards support full widget customization:

// Custom title widget
SmartSuccessCard(
  titleWidget: Row(
    mainAxisAlignment: MainAxisAlignment.center,
    children: [
      Icon(Icons.verified, color: Colors.green),
      SizedBox(width: 8),
      Text('Verified!', style: TextStyle(fontWeight: FontWeight.bold)),
    ],
  ),
  message: 'Your account has been verified',
)

// Custom body widget
SmartInfoCard(
  title: 'Update Available',
  bodyWidget: Column(
    children: [
      Text('Version 2.0.0 is now available'),
      SizedBox(height: 8),
      Text('• New features\n• Bug fixes'),
    ],
  ),
  action: 'Update Now',
  onActionPressed: () => update(),
)

// Custom actions widget
SmartErrorCard(
  title: 'Connection Failed',
  message: 'Unable to connect to server',
  actionsWidget: Column(
    children: [
      FilledButton.icon(
        onPressed: () => retry(),
        icon: Icon(Icons.refresh),
        label: Text('Retry'),
      ),
      TextButton(
        onPressed: () => workOffline(),
        child: Text('Work Offline'),
      ),
    ],
  ),
)

// Custom icon widget
SmartInfoCard(
  title: 'Premium Feature',
  iconWidget: Container(
    padding: EdgeInsets.all(12),
    decoration: BoxDecoration(
      gradient: LinearGradient(colors: [Colors.purple, Colors.blue]),
      borderRadius: BorderRadius.circular(12),
    ),
    child: Icon(Icons.star, color: Colors.white),
  ),
  message: 'Upgrade to unlock',
)

Close Button #

Cards can display a close button:

SmartInfoCard(
  title: 'Notification',
  message: 'You have a new message',
  showCloseButton: true,
  onClose: () => dismiss(),
  closeButtonColor: Colors.grey,
)

// From exception with close button
SmartErrorCard.fromException(
  exception,
  showCloseButton: true,
  onClose: () => dismiss(),
  action: 'Retry',
  onActionPressed: () => retry(),
)

Connectivity Checker #

Check network connectivity:

// Check connection
final hasConnection = await ConnectivityChecker.hasConnection();

// Get connection type
final status = await ConnectivityChecker.getStatus();

// Check specific connection type
final isWifi = await ConnectivityChecker.isConnectedViaWifi();
final isMobile = await ConnectivityChecker.isConnectedViaMobile();

// Listen to changes
ConnectivityChecker.onConnectivityChanged.listen((results) {
  if (results.contains(ConnectivityResult.none)) {
    showOfflineIndicator();
  }
});

API Reference #

SmartExecuter Methods #

Method Description
execute<T>() Execute and return Result (no UI)
run<T>() Execute with loading dialog
inBackground<T>() Execute without dialog
runStream<T>() Execute stream with dialog
inBackgroundStream<T>() Execute stream without dialog

ExecuterOptions #

Property Type Default Description
showLoadingDialog bool true Show loading dialog
checkConnection bool? null Check connectivity
maxRetries int? null Max retry attempts
retryDelay Duration? null Delay between retries
timeout Duration? null Operation timeout
loadingWidget Widget? null Custom loading widget
barrierDismissible bool false Can dismiss dialog
barrierColor Color? null Dialog barrier color
operationName String? null Operation name for debugging
metadata Map<String, dynamic>? null Custom metadata for exceptions

Migration from 0.x #

If you're migrating from an earlier version:

  1. Replace ErrorInfoBar with SmartErrorSnackBar
  2. Replace AppWaitingDialog with SmartLoadingDialog
  3. Add SmartExecuterConfig.initialize() in main.dart
  4. Update callback signatures (now use SmartException)

Contributing #

Contributions are welcome! Please read our contributing guidelines first.

  1. Fork the repository
  2. Create your feature branch (git checkout -b feature/amazing-feature)
  3. Commit your changes (git commit -m 'Add amazing feature')
  4. Push to the branch (git push origin feature/amazing-feature)
  5. Open a Pull Request

License #

This project is licensed under the MIT License - see the LICENSE file for details.

Support #

If you find this package helpful, please give it a star on GitHub!

For bugs or feature requests, please open an issue.

1
likes
150
points
23
downloads

Publisher

unverified uploader

Weekly Downloads

A powerful Flutter package for executing async operations with built-in error handling, loading dialogs, retry logic, and Result pattern support.

Repository (GitHub)
View/report issues

Topics

#async #error-handling #loading #network #dio

Documentation

API reference

License

MIT (license)

Dependencies

connectivity_plus, dio, flutter, logger

More

Packages that depend on smart_executer