smart_executer 1.3.0+2
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 #
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:
- Replace
ErrorInfoBarwithSmartErrorSnackBar - Replace
AppWaitingDialogwithSmartLoadingDialog - Add
SmartExecuterConfig.initialize()in main.dart - Update callback signatures (now use
SmartException)
Contributing #
Contributions are welcome! Please read our contributing guidelines first.
- Fork the repository
- Create your feature branch (
git checkout -b feature/amazing-feature) - Commit your changes (
git commit -m 'Add amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - 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.