12 KiB
API Client Setup - Complete Implementation
Overview
I have created a robust API client for your Flutter warehouse management app with comprehensive features including:
- Automatic token management from secure storage
- 401 error handling with automatic token clearing
- Request/response logging with sensitive data redaction
- Configurable timeouts (30 seconds)
- Proper error transformation to custom exceptions
- Support for all HTTP methods (GET, POST, PUT, DELETE)
Files Created
1. Core Network Files
/lib/core/network/api_client.dart
- Main API client implementation using Dio
- Automatic Bearer token injection from secure storage
- Request/response/error interceptors with comprehensive logging
- 401 error handler that clears tokens and triggers logout callback
- Methods:
get(),post(),put(),delete() - Utility methods:
testConnection(),isAuthenticated(),getAccessToken(),clearAuth()
/lib/core/network/api_response.dart
- Generic API response wrapper matching your backend format
- Structure:
Value,IsSuccess,IsFailure,Errors,ErrorCodes - Helper methods:
hasData,getErrorMessage(),getAllErrorsAsString(),hasErrorCode()
/lib/core/network/api_client_example.dart
- Comprehensive usage examples for all scenarios
- Examples for: Login, GET/POST/PUT/DELETE requests, error handling, etc.
/lib/core/network/README.md
- Complete documentation for the API client
- Usage guides, best practices, troubleshooting
2. Secure Storage
/lib/core/storage/secure_storage.dart
- Singleton wrapper for flutter_secure_storage
- Token management:
saveAccessToken(),getAccessToken(),clearTokens() - User data:
saveUserId(),saveUsername(), etc. - Utility methods:
isAuthenticated(),clearAll(),containsKey() - Platform-specific secure options (Android: encrypted shared prefs, iOS: Keychain)
3. Constants
/lib/core/constants/api_endpoints.dart
- Centralized API endpoint definitions
- Authentication:
/auth/login,/auth/logout - Warehouses:
/warehouses - Products:
/productswith query parameter helpers - Scans:
/api/scans
4. Core Exports
/lib/core/core.dart (Updated)
- Added exports for new modules:
api_endpoints.dartapi_response.dartsecure_storage.dart
5. Dependencies
pubspec.yaml (Updated)
- Added
flutter_secure_storage: ^9.0.0
Key Features
1. Automatic Token Management
The API client automatically injects Bearer tokens into all requests:
// Initialize with secure storage
final secureStorage = SecureStorage();
final apiClient = ApiClient(secureStorage);
// Token is automatically added to all requests
final response = await apiClient.get('/warehouses');
// Request header: Authorization: Bearer <token>
2. 401 Error Handling
When a 401 Unauthorized error occurs:
- Error is logged to console
- All tokens are cleared from secure storage
onUnauthorizedcallback is triggered- App can navigate to login screen
final apiClient = ApiClient(
secureStorage,
onUnauthorized: () {
// This callback is triggered on 401 errors
context.go('/login'); // Navigate to login
},
);
3. Comprehensive Logging
All requests, responses, and errors are logged with sensitive data redacted:
REQUEST[GET] => https://api.example.com/warehouses
Headers: {Authorization: ***REDACTED***, Content-Type: application/json}
RESPONSE[200] => https://api.example.com/warehouses
Data: {...}
ERROR[401] => https://api.example.com/products
401 Unauthorized - Clearing tokens and triggering logout
4. Error Handling
Dio exceptions are transformed into custom app exceptions:
try {
final response = await apiClient.get('/products');
} on NetworkException catch (e) {
// Timeout, no internet, etc.
print('Network error: ${e.message}');
} on ServerException catch (e) {
// 4xx, 5xx errors
print('Server error: ${e.message}');
print('Error code: ${e.code}'); // e.g., '401', '404', '500'
}
Specific error codes:
401: Unauthorized (automatically handled)403: Forbidden404: Not Found422: Validation Error429: Rate Limited500-504: Server Errors
5. API Response Parsing
All API responses follow the standard format:
final response = await apiClient.get('/warehouses');
final apiResponse = ApiResponse.fromJson(
response.data,
(json) => (json as List).map((e) => Warehouse.fromJson(e)).toList(),
);
if (apiResponse.isSuccess && apiResponse.value != null) {
final warehouses = apiResponse.value;
print('Found ${warehouses.length} warehouses');
} else {
print('Error: ${apiResponse.getErrorMessage()}');
}
Usage Examples
Initialize API Client
import 'package:minhthu/core/core.dart';
final secureStorage = SecureStorage();
final apiClient = ApiClient(
secureStorage,
onUnauthorized: () {
// Navigate to login on 401 errors
context.go('/login');
},
);
Login Flow
// 1. Login request
final response = await apiClient.post(
ApiEndpoints.login,
data: {
'username': 'user@example.com',
'password': 'password123',
},
);
// 2. Parse response
final apiResponse = ApiResponse.fromJson(
response.data,
(json) => User.fromJson(json),
);
// 3. Save tokens (typically done in LoginUseCase)
if (apiResponse.isSuccess && apiResponse.value != null) {
final user = apiResponse.value!;
await secureStorage.saveAccessToken(user.accessToken);
await secureStorage.saveRefreshToken(user.refreshToken);
await secureStorage.saveUserId(user.userId);
}
Get Warehouses (Authenticated)
// Token is automatically added by the interceptor
final response = await apiClient.get(ApiEndpoints.warehouses);
final apiResponse = ApiResponse.fromJson(
response.data,
(json) => (json as List).map((e) => Warehouse.fromJson(e)).toList(),
);
if (apiResponse.isSuccess && apiResponse.value != null) {
final warehouses = apiResponse.value!;
// Use the data
}
Get Products with Query Parameters
final response = await apiClient.get(
ApiEndpoints.products,
queryParameters: ApiEndpoints.productQueryParams(
warehouseId: 1,
type: 'import',
),
);
Save Scan Data
final response = await apiClient.post(
ApiEndpoints.scans,
data: {
'barcode': '1234567890',
'field1': 'Value 1',
'field2': 'Value 2',
'field3': 'Value 3',
'field4': 'Value 4',
},
);
Check Authentication Status
final isAuthenticated = await apiClient.isAuthenticated();
if (!isAuthenticated) {
// Navigate to login
}
Logout
await apiClient.clearAuth(); // Clears all tokens
Integration with Repository Pattern
The API client is designed to work with your clean architecture:
class WarehouseRemoteDataSourceImpl implements WarehouseRemoteDataSource {
final ApiClient apiClient;
WarehouseRemoteDataSourceImpl(this.apiClient);
@override
Future<List<Warehouse>> getWarehouses() async {
final response = await apiClient.get(ApiEndpoints.warehouses);
final apiResponse = ApiResponse.fromJson(
response.data,
(json) => (json as List).map((e) => Warehouse.fromJson(e)).toList(),
);
if (apiResponse.isSuccess && apiResponse.value != null) {
return apiResponse.value!;
} else {
throw ServerException(apiResponse.getErrorMessage());
}
}
}
Configuration
Timeouts (in app_constants.dart)
static const int connectionTimeout = 30000; // 30 seconds
static const int receiveTimeout = 30000; // 30 seconds
static const int sendTimeout = 30000; // 30 seconds
Base URL (in app_constants.dart)
static const String apiBaseUrl = 'https://api.example.com';
Or update dynamically:
apiClient.updateBaseUrl('https://dev-api.example.com');
Security Features
-
Token Encryption: Tokens stored using platform-specific secure storage
- Android: Encrypted SharedPreferences
- iOS: Keychain with
first_unlockaccessibility
-
Automatic Token Clearing: 401 errors automatically clear all tokens
-
Log Sanitization: Authorization headers redacted in logs
Headers: {Authorization: ***REDACTED***} -
Singleton Pattern: SecureStorage uses singleton to prevent multiple instances
Testing
To test the API connection:
final isConnected = await apiClient.testConnection();
if (!isConnected) {
print('Cannot connect to API');
}
Dependency Injection (GetIt)
Register with GetIt service locator:
final getIt = GetIt.instance;
// Register SecureStorage
getIt.registerLazySingleton<SecureStorage>(() => SecureStorage());
// Register ApiClient
getIt.registerLazySingleton<ApiClient>(
() => ApiClient(
getIt<SecureStorage>(),
onUnauthorized: () {
// Handle unauthorized
},
),
);
File Structure
lib/
core/
constants/
app_constants.dart # Existing - timeouts and base URL
api_endpoints.dart # NEW - API endpoint constants
network/
api_client.dart # UPDATED - Full implementation
api_response.dart # NEW - API response wrapper
api_client_example.dart # NEW - Usage examples
README.md # NEW - Documentation
storage/
secure_storage.dart # NEW - Secure storage wrapper
errors/
exceptions.dart # Existing - Used by API client
failures.dart # Existing - Used by repositories
core.dart # UPDATED - Added new exports
Next Steps
- Update Existing Repositories: Update your remote data sources to use the new API client
- Configure Base URL: Set the correct API base URL in
app_constants.dart - Set Up Navigation: Implement the
onUnauthorizedcallback to navigate to login - Add API Endpoints: Add any missing endpoints to
api_endpoints.dart - Test Authentication Flow: Test login, token injection, and 401 handling
Testing the Setup
Run the example:
import 'package:minhthu/core/network/api_client_example.dart';
void main() async {
await runExamples();
}
Troubleshooting
Token not being injected
- Verify token is saved:
await secureStorage.getAccessToken() - Check logs for:
REQUEST[...] Headers: {Authorization: ***REDACTED***}
401 errors not clearing tokens
- Verify
onUnauthorizedcallback is set - Check logs for:
401 Unauthorized - Clearing tokens and triggering logout
Connection timeouts
- Check network connectivity
- Verify base URL is correct
- Increase timeout values if needed
Analysis Results
All files pass Flutter analysis with no issues:
- ✅
api_client.dart- No issues found - ✅
secure_storage.dart- No issues found - ✅
api_response.dart- No issues found - ✅
api_endpoints.dart- No issues found
Documentation
For detailed documentation, see:
/lib/core/network/README.md- Complete API client documentation/lib/core/network/api_client_example.dart- Code examples
Summary
The API client is production-ready with:
- ✅ Automatic token management from secure storage
- ✅ Request interceptor to inject Bearer tokens
- ✅ Response interceptor for logging
- ✅ Error interceptor to handle 401 errors
- ✅ Automatic token clearing on unauthorized access
- ✅ Comprehensive error handling
- ✅ Request/response logging with sensitive data redaction
- ✅ Support for all HTTP methods (GET, POST, PUT, DELETE)
- ✅ Configurable timeouts (30 seconds)
- ✅ Environment-specific base URLs
- ✅ Connection testing
- ✅ Clean integration with repository pattern
- ✅ Comprehensive documentation and examples
- ✅ All files pass static analysis
The API client is ready to use and follows Flutter best practices and clean architecture principles!