ApiFlow Usage Guide

Overview

ApiFlow provides a functional programming approach to HTTP operations using the Either pattern. Instead of throwing exceptions, it returns explicit success or error results, making error handling more predictable and type-safe.

The Either Pattern

The Either pattern represents a value that can be one of two types:

  • Left: Contains error information (ServiceErrorModel)
  • Right: Contains success data (ResponseResultsModel<T>)

This eliminates unexpected exceptions and forces explicit error handling.

Quick Setup

// Initialize once in your app
await EitherApiService.init(
  baseUrl: 'https://api.example.com',
  enableCaching: true,
  boxNameCaching: 'api_cache',
  enableLogging: true,
);

Basic Usage Pattern

// Make a request
final result = await EitherApiService.get<User>(
  endpoint: '/users/123',
  fromJson: (json) => User.fromJson(json),
);

// Handle the result
result.fold(
  (error) => print('Error: ${error.message}'),
  (success) => print('User: ${success.dataFromApi?.name}'),
);

HTTP Methods

GET Request

Used for retrieving data with automatic caching support.

final result = await EitherApiService.get<List<User>>(
  endpoint: '/users',
  fromJson: (json) => (json as List).map((e) => User.fromJson(e)).toList(),
  queryParameters: {'page': 1, 'limit': 10},
  enableCachingRequest: true, // Override global caching setting
);

// Pattern matching
if (result.isRight) {
  final users = result.right.dataFromApi;
  print('Found ${users?.length} users');
} else {
  final error = result.left;
  print('Error ${error.statusCode}: ${error.message}');
}

POST Request

Used for creating new resources with automatic list cache invalidation.

final newUser = User(name: 'John', email: 'john@example.com');

final result = await EitherApiService.post<User>(
  endpoint: '/users',
  data: newUser.toJson(),
  fromJson: (json) => User.fromJson(json),
  invalidateListCache: true, // Clear related list caches
);

result.fold(
  (error) => showError('Failed to create user: ${error.message}'),
  (success) => showSuccess('User created: ${success.dataFromApi?.name}'),
);

PUT Request

Used for updating existing resources with automatic cache updates.

final updatedUser = user.copyWith(name: 'John Updated');

final result = await EitherApiService.put<User>(
  endpoint: '/users/${user.id}',
  data: updatedUser.toJson(),
  fromJson: (json) => User.fromJson(json),
  updateCache: true, // Update cache with new data
);

result.fold(
  (error) => handleError(error),
  (success) => updateUI(success.dataFromApi),
);

DELETE Request

Used for removing resources with automatic cache cleanup.

final result = await EitherApiService.delete<void>(
  endpoint: '/users/${user.id}',
  invalidateCache: true, // Remove from cache
);

result.fold(
  (error) => showError('Delete failed: ${error.message}'),
  (success) => showSuccess('User deleted successfully'),
);

Key Parameters

Common Parameters

Parameter Type Description
endpoint String API endpoint path (required)
fromJson T Function(dynamic)? Function to convert JSON to your model
queryParameters Map<String, dynamic>? URL query parameters
options Options? Dio request options (headers, timeout, etc.)
cancelToken CancelToken? Token to cancel the request

Caching Parameters

Parameter Type Description
enableCachingRequest bool? Override global caching for GET requests
invalidateListCache bool? Clear related list cache after POST
updateCache bool? Update cache with new data after PUT
invalidateCache bool? Remove cached data after DELETE

Progress Tracking

Parameter Type Description
onReceiveProgress ProgressCallback? Track download progress
onSendProgress ProgressCallback? Track upload progress

Error Handling Patterns

Using fold() Method

final result = await EitherApiService.get<User>(endpoint: '/user/123');

result.fold(
  (error) {
    // Handle different error types
    switch (error.type) {
      case ErrorType.noInternet:
        showRetryDialog();
        break;
      case ErrorType.unauthorized:
        redirectToLogin();
        break;
      case ErrorType.serverError:
        showMaintenanceMessage();
        break;
      default:
        showGenericError(error.message);
    }
  },
  (success) {
    // Handle success
    final user = success.dataFromApi;
    updateUserProfile(user);
  },
);

Using isLeft/isRight Properties

final result = await EitherApiService.get<User>(endpoint: '/user/123');

if (result.isLeft) {
  final error = result.left;
  print('Request failed: ${error.message}');
  
  // Access detailed error info
  print('Status Code: ${error.statusCode}');
  print('Error Type: ${error.type.name}');
  
  if (error.hasDetails) {
    print('Request URL: ${error.details!.fullUrl}');
    print('Duration: ${error.details!.calculatedDuration}');
  }
} else {
  final success = result.right;
  final user = success.dataFromApi;
  
  // Access response details
  print('Status: ${success.dioDetails.statusCode}');
  print('Duration: ${success.dioDetails.calculatedDuration}ms');
  print('From Cache: ${success.dioDetails.extra.containsKey('cached')}');
}

Advanced Usage

Custom Error Handling

Future<User?> getUserSafely(int userId) async {
  final result = await EitherApiService.get<User>(
    endpoint: '/users/$userId',
    fromJson: (json) => User.fromJson(json),
  );
  
  return result.fold(
    (error) {
      // Log error for debugging
      logger.error('Failed to get user $userId: ${error.message}');
      
      // Return null for safe handling
      return null;
    },
    (success) => success.dataFromApi,
  );
}

Chaining Requests

Future<Either<ServiceErrorModel, UserProfile>> getUserProfile(int userId) async {
  final userResult = await EitherApiService.get<User>(
    endpoint: '/users/$userId',
    fromJson: (json) => User.fromJson(json),
  );
  
  if (userResult.isLeft) {
    return Left(userResult.left);
  }
  
  final user = userResult.right.dataFromApi!;
  
  final profileResult = await EitherApiService.get<Profile>(
    endpoint: '/users/$userId/profile',
    fromJson: (json) => Profile.fromJson(json),
  );
  
  return profileResult.fold(
    (error) => Left(error),
    (success) => Right(UserProfile(
      user: user,
      profile: success.dataFromApi!,
    )),
  );
}

Batch Operations

Future<List<User>> getMultipleUsers(List<int> userIds) async {
  final futures = userIds.map((id) => EitherApiService.get<User>(
    endpoint: '/users/$id',
    fromJson: (json) => User.fromJson(json),
  ));
  
  final results = await Future.wait(futures);
  
  return results
    .where((result) => result.isRight)
    .map((result) => result.right.dataFromApi!)
    .toList();
}

Initialization Options

await EitherApiService.init(
  // Required
  baseUrl: 'https://api.example.com',
  
  // Caching
  boxNameCaching: 'api_cache',      // Cache storage name
  enableCaching: true,               // Enable/disable caching
  
  // Authentication
  getToken: () => AuthService.getToken(), // Custom token provider
  onUnauthorized: () => AuthService.logout(), // 401 handler
  
  // Network
  connectTimeout: Duration(seconds: 15),
  receiveTimeout: Duration(seconds: 30),
  
  // Headers
  headers: {
    'App-Version': '1.0.0',
    'Platform': Platform.isIOS ? 'ios' : 'android',
  },
  
  // Debug
  enableLogging: kDebugMode,         // Enable in debug mode only
);

Best Practices

  1. Always handle both sides of Either

    // Good
    result.fold(
      (error) => handleError(error),
      (success) => handleSuccess(success),
    );
       
    // Avoid
    if (result.isRight) {
      // Only handling success
    }
    
  2. Use meaningful error messages

    result.fold(
      (error) => showSnackbar('Failed to save changes. Please try again.'),
      (success) => showSnackbar('Changes saved successfully!'),
    );
    
  3. Leverage caching appropriately

    // Cache frequently accessed, rarely changing data
    final staticData = await EitherApiService.get<Config>(
      endpoint: '/config',
      enableCachingRequest: true,
    );
       
    // Don't cache dynamic or user-specific data
    final notifications = await EitherApiService.get<List<Notification>>(
      endpoint: '/notifications',
      enableCachingRequest: false,
    );
    
  4. Use proper generic types

    // Good - specific type
    EitherApiService.get<List<User>>()
       
    // Avoid - dynamic type
    EitherApiService.get()
    

This pattern ensures predictable error handling while maintaining clean, readable code!