cyber_req 2.0.6
cyber_req: ^2.0.6 copied to clipboard
A flexible API client for Laravel backends with dynamic headers, bearer token support (including FlutterSecureStorage integration), and callbacks for success, failure, and unauthorized handling.
cyber_req #
A flexible and robust API client for Laravel backends, designed to streamline your Flutter application's network interactions. It offers dynamic header management, secure bearer token handling with flutter_secure_storage, and comprehensive callback mechanisms for success, failure, and unauthorized access.
Features #
- Dynamic Headers: Effortlessly add or override HTTP headers for individual requests, or set global default headers for consistent API communication.
- Secure Bearer Token Management: Seamlessly integrate bearer tokens into your requests, with built-in support for fetching and storing tokens securely using
flutter_secure_storage. - Laravel Backend Optimization: Crafted with common Laravel API patterns in mind, ensuring smooth handling of JSON responses and efficient error propagation.
- Granular Callbacks: Utilize
onSuccess,onFailure, andonUnauthorizedcallbacks to gain fine-grained control over API responses and error handling logic. - Custom Exception Handling: Benefit from dedicated
ApiExceptionandUnauthorizedExceptionclasses for robust and predictable error management. - Comprehensive HTTP Method Support: Perform
POST,GET,PUT, andDELETErequests with ease. - Intelligent Network Error Handling: Automatically catches and reports common network issues such as lack of internet connectivity or invalid response formats.
- Automatic Request/Response Logging: Optionally log comprehensive request details (URL, method, headers, payload, query parameters) and full response details (URL, status code, body) directly to the console for easy debugging. This is controlled by the
autoLogResponseparameter.
Installation #
Add cyber_req to your pubspec.yaml file:
dependencies:
cyber_req: ^2.0.0 # Use the latest version
http: ^0.13.5 # Required by cyber_req, good to explicitly list if used elsewhere
flutter_secure_storage: ^8.0.0 # Essential for secure token storage
After adding the dependencies, run flutter pub get in your terminal to fetch the packages.
Quick Start #
Here's a minimal example to get you up and running with cyber_req:
import 'package:cyber_req/cyber_req.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
void main() async {
// 1. Initialize FlutterSecureStorage (if using secure tokens)
final storage = FlutterSecureStorage();
// 2. Define an unauthorized handler (optional but recommended)
Future<void> handleUnauthorized() async {
print('User unauthorized. Clearing token and redirecting to login.');
await storage.delete(key: 'token'); // Clear token
// Navigate to login screen or refresh token
}
// 3. Initialize ApiService
final apiService = ApiService(
baseUrl: 'https://api.example.com', // Replace with your API base URL
onUnauthorized: handleUnauthorized,
autoLogResponse: true, // Enable/disable automatic logging (default is true)
);
// 4. Make a simple GET request
try {
final response = await apiService.get(
'posts',
useStorageToken: true, // Attempt to use a token from secure storage
);
print('Successfully fetched posts: $response');
} on ApiException catch (e) {
print('API Error: ${e.message} (Status: ${e.statusCode})');
} catch (e) {
print('An unexpected error occurred: $e');
} finally {
apiService.dispose(); // Don't forget to dispose!
}
}
Usage #
Initialization of ApiService #
The ApiService is the core of cyber_req. You initialize it with your API's base URL and can configure it further with various optional parameters.
import 'package:cyber_req/cyber_req.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:http/http.dart' as http; // Only if you need a custom http.Client
final storage = FlutterSecureStorage();
// Example unauthorized handler: This function will be called when a 401 Unauthorized response is received.
Future<void> myUnauthorizedHandler() async {
print('Authentication failed! User needs to re-authenticate.');
await storage.delete(key: 'token'); // Clear any invalid token
// Implement navigation to login screen or token refresh logic here.
}
final ApiService apiService = ApiService(
baseUrl: 'https://your-laravel-backend.com/api', // **Required**: Your API's base URL.
bearerToken: 'your_static_bearer_token_if_any', // Optional: A static bearer token to be used for all requests.
// Prefer `useStorageToken` for dynamic tokens.
httpClient: http.Client(), // Optional: Provide a custom http.Client instance. Useful for testing or custom configurations.
onUnauthorized: myUnauthorizedHandler, // Optional: Callback function for 401 Unauthorized responses.
defaultHeaders: { // Optional: Headers that will be sent with every request.
'Content-Type': 'application/json',
'X-App-Version': '1.0.0',
},
autoLogResponse: true, // Optional: Set to `false` to disable automatic logging of requests and responses. Defaults to `true`.
);
Making Requests #
ApiService provides methods for common HTTP verbs: post, get, put, and delete. Each method returns a Future<Map<String, dynamic>> representing the JSON response from your API.
Common Request Parameters
All request methods (post, get, put, delete) share several optional parameters for fine-grained control:
data: (Map<String, dynamic>) ForPOSTandPUTrequests, this map will be JSON encoded and sent as the request body.queryParams: (Map<String, dynamic>) ForGETrequests, these parameters will be appended to the URL as query strings.extraHeaders: (Map<String, String>) Headers specific to this request. These will overridedefaultHeadersif there are conflicts.bearerToken: (String?) A bearer token to use for this specific request. If provided, it takes precedence over theApiService'sbearerTokenand any token fromflutter_secure_storage.useStorageToken: (bool, defaults tofalse) Iftrue,cyber_reqwill attempt to read a token fromflutter_secure_storageunder the key'token'and use it as theAuthorizationheader.onSuccess: (SuccessCallback?) A callback functionvoid Function(Map<String, dynamic> data)that is executed when the request is successful (HTTP status 2xx) and the response body is successfully decoded.onFailure: (FailureCallback?) A callback functionvoid Function(ApiException error)that is executed when the request fails (non-2xx status, network error, or invalid response format).allowedStatusCodes: (List<int>?) A list of additional HTTP status codes (beyond 2xx) that should be considered successful. For example, if your API returns201 Createdor204 No Contentfor success, you might include them here.
POST Request Example
Future<void> createNewUser(String name, String email, String password) async {
try {
final response = await apiService.post(
'register', // Your API endpoint
data: {
'name': name,
'email': email,
'password': password,
},
extraHeaders: {
'X-Request-ID': 'unique-id-123', // Example of an extra header
},
// No useStorageToken here, as this might be a registration endpoint
onSuccess: (data) {
print('User registered successfully: $data');
// Save the token if returned by the API
if (data.containsKey('token')) {
storage.write(key: 'token', value: data['token']);
print('Token saved to secure storage.');
}
},
onFailure: (error) {
print('Registration failed: ${error.message}');
// Handle specific error messages from the backend
},
);
print('Raw POST response: $response');
} on ApiException catch (e) {
print('API Exception during POST: ${e.message} (Status: ${e.statusCode})');
} on UnauthorizedException {
print('Unauthorized access during POST. This should not happen for registration.');
} catch (e) {
print('An unexpected error occurred during POST: $e');
}
}
GET Request Example
Future<void> fetchUserProfile(String userId) async {
try {
final response = await apiService.get(
'users/$userId', // Endpoint with path parameter
queryParams: {
'include_posts': 'true', // Example query parameter
'limit': '5',
},
useStorageToken: true, // Use the token from FlutterSecureStorage
onSuccess: (data) {
print('User profile fetched: ${data['user']['name']}');
},
);
print('Raw GET response: $response');
} on ApiException catch (e) {
print('API Exception during GET: ${e.message} (Status: ${e.statusCode})');
} catch (e) {
print('An unexpected error occurred during GET: $e');
}
}
PUT Request Example
Future<void> updateProduct(String productId, Map<String, dynamic> updates) async {
try {
final response = await apiService.put(
'products/$productId',
data: updates,
useStorageToken: true,
onSuccess: (data) {
print('Product updated successfully: $data');
},
);
print('Raw PUT response: $response');
} on ApiException catch (e) {
print('API Exception during PUT: ${e.message} (Status: ${e.statusCode})');
} catch (e) {
print('An unexpected error occurred during PUT: $e');
}
}
DELETE Request Example
Future<void> removeComment(String commentId) async {
try {
final response = await apiService.delete(
'comments/$commentId',
useStorageToken: true,
allowedStatusCodes: [204], // API might return 204 No Content for successful deletion
onSuccess: (data) {
print('Comment deleted successfully.');
},
);
print('Raw DELETE response: $response');
} on ApiException catch (e) {
print('API Exception during DELETE: ${e.message} (Status: ${e.statusCode})');
} catch (e) {
print('An unexpected error occurred during DELETE: $e');
}
}
Multipart POST Request Example
For uploading files, cyber_req provides the postMultipart method. This method is designed to handle multipart/form-data requests, allowing you to send files along with other form fields.
First, define MultipartFile objects for each file you want to upload:
import 'dart:io';
import 'package:cyber_req/cyber_req.dart'; // Ensure this import is present
Future<void> uploadProfilePicture(String userId, File imageFile) async {
try {
final response = await apiService.postMultipart(
'users/$userId/profile-picture', // Your API endpoint for file upload
files: [
MultipartFile(
field: 'profile_picture', // The field name expected by your backend for the file
file: imageFile,
filename: 'profile.jpg', // Optional: specify a filename, otherwise it's inferred
),
],
fields: {
'description': 'User profile picture upload', // Optional: additional text fields
'user_id': userId,
},
useStorageToken: true, // Use the token from FlutterSecureStorage
onSuccess: (data) {
print('Profile picture uploaded successfully: $data');
},
onFailure: (error) {
print('Profile picture upload failed: ${error.message}');
},
);
print('Raw Multipart POST response: $response');
} on ApiException catch (e) {
print('API Exception during Multipart POST: ${e.message} (Status: ${e.statusCode})');
} catch (e) {
print('An unexpected error occurred during Multipart POST: $e');
}
}
MultipartFile Class:
The MultipartFile class is a simple data structure used to define the files you want to upload:
class MultipartFile {
final String field; // The name of the form field for this file (e.g., 'image', 'document')
final File file; // The actual file to be uploaded
final String? filename; // Optional: The filename to send to the server. If null, inferred from file path.
MultipartFile({
required this.field,
required this.file,
this.filename,
});
}
Token Management with flutter_secure_storage #
cyber_req integrates seamlessly with flutter_secure_storage for secure token persistence.
Saving a Token:
After a successful login or token refresh, save the token:
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
final storage = FlutterSecureStorage();
Future<void> saveAuthToken(String token) async {
await storage.write(key: 'token', value: token);
print('Authentication token saved securely.');
}
Using a Stored Token in Requests:
Set useStorageToken: true in your request methods:
// This will automatically fetch the token from secure storage (key: 'token')
// and include it in the Authorization header.
final response = await apiService.get('protected-data', useStorageToken: true);
Clearing a Token (e.g., on Logout or Unauthorized):
Future<void> clearAuthToken() async {
await storage.delete(key: 'token');
print('Authentication token cleared from secure storage.');
}
Error Handling #
cyber_req provides robust error handling through custom exceptions. It's crucial to wrap your API calls in try-catch blocks to manage different error scenarios gracefully.
ApiException: The base exception for all API-related errors. It contains amessage(from the backend or a generic error) and an optionalstatusCode.UnauthorizedException: A specific subclass ofApiExceptionthat is thrown when an HTTP 401 (Unauthorized) status code is received. This is particularly useful for triggering re-authentication flows.
cyber_req also provides intelligent handling for common network and format errors, throwing specific exceptions for easier debugging and user feedback:
SocketException: Thrown for network connectivity issues (e.g., no internet connection).HttpException: Thrown for general HTTP errors during communication with the server.FormatException: Thrown when the server response is not a valid JSON format.
try {
// Attempt to fetch data that requires authentication
final data = await apiService.get('user/dashboard', useStorageToken: true);
print('Dashboard data: $data');
} on UnauthorizedException {
// Handle 401 Unauthorized specifically
print('Access denied. Your session may have expired. Please log in again.');
// Trigger your app's logout or token refresh flow
await storage.delete(key: 'token'); // Clear invalid token
// Navigator.pushReplacementNamed(context, '/login');
} on ApiException catch (e) {
// Handle other API errors (e.g., 400 Bad Request, 404 Not Found, 500 Internal Server Error)
print('An API error occurred: ${e.message} (Status Code: ${e.statusCode})');
if (e.statusCode == 404) {
print('Resource not found.');
} else if (e.statusCode == 403) {
print('Permission denied.');
}
// Display an error message to the user
} on SocketException {
// Handle network connectivity issues (e.g., no internet)
print('Network error: Please check your internet connection.');
} on HttpException {
// Handle general HTTP errors during communication
print('HTTP error: Failed to communicate with server.');
} on FormatException {
// Handle cases where the server response is not valid JSON
print('Invalid response format from server.');
} catch (e) {
// Catch any other unexpected errors
print('An unexpected error occurred: $e');
}
Disposing the Client #
It's a good practice to dispose of the ApiService's internal http.Client when it's no longer needed to prevent memory leaks and ensure resources are properly released. This is especially important in stateful widgets.
class MyWidget extends StatefulWidget {
@override
_MyWidgetState createState() => _MyWidgetState();
}
class _MyWidgetState extends State<MyWidget> {
late final ApiService apiService;
@override
void initState() {
super.initState();
apiService = ApiService(baseUrl: 'https://api.example.com');
}
@override
void dispose() {
apiService.dispose(); // Call dispose when the widget is removed from the tree
super.dispose();
}
@override
Widget build(BuildContext context) {
// ... your widget build method
return Container();
}
}
API Reference (Brief) #
ApiService({required String baseUrl, String? bearerToken, http.Client? httpClient, UnauthorizedHandler? onUnauthorized, Map<String, String>? defaultHeaders, bool autoLogResponse = true}): Constructor for the API service.Future<Map<String, dynamic>> post(String endpoint, {Map<String, dynamic>? data, Map<String, String>? extraHeaders, String? bearerToken, bool useStorageToken, SuccessCallback? onSuccess, FailureCallback? onFailure, List<int>? allowedStatusCodes}): Sends a POST request.Future<Map<String, dynamic>> get(String endpoint, {Map<String, dynamic>? queryParams, Map<String, String>? extraHeaders, String? bearerToken, bool useStorageToken, SuccessCallback? onSuccess, FailureCallback? onFailure, List<int>? allowedStatusCodes}): Sends a GET request.Future<Map<String, dynamic>> put(String endpoint, {Map<String, dynamic>? data, Map<String, String>? extraHeaders, String? bearerToken, bool useStorageToken, SuccessCallback? onSuccess, FailureCallback? onFailure, List<int>? allowedStatusCodes}): Sends a PUT request.Future<Map<String, dynamic>> delete(String endpoint, {Map<String, String>? extraHeaders, String? bearerToken, bool useStorageToken, SuccessCallback? onSuccess, FailureCallback? onFailure, List<int>? allowedStatusCodes}): Sends a DELETE request.Future<Map<String, dynamic>> postMultipart(String endpoint, {required List<MultipartFile> files, Map<String, String>? fields, Map<String, String>? extraHeaders, String? bearerToken, bool useStorageToken, SuccessCallback? onSuccess, FailureCallback? onFailure, List<int>? allowedStatusCodes}): Sends a multipart POST request with files and optional fields.MultipartFile({required String field, required File file, String? filename}): Constructor for defining a file to be uploaded in a multipart request.void dispose(): Closes the internal HTTP client.
Contributing #
Contributions are welcome! If you find a bug or have a feature request, please open an issue on the GitHub repository.
License #
This project is licensed under the MIT License.
Support and Donations #
If you find this package useful and would like to support its continued development, consider a donation!
Opay Account: 8169795832
WhatsApp: Chat with me on WhatsApp
cyber_req is maintained by cyberwizard-dev.