17 KiB
API Integration Guide - Retail POS App
Overview
This guide provides comprehensive documentation for the API integration layer of the Retail POS application. The integration is built using Dio for HTTP requests with a robust error handling, retry logic, and offline-first architecture.
Architecture
Layered Architecture
Presentation Layer (UI)
“
Providers (Riverpod)
“
Use Cases
“
Repositories
“
Data Sources (Remote + Local)
“
Network Layer (Dio)
Components
1. DioClient (/lib/core/network/dio_client.dart)
Purpose: Centralized HTTP client configuration with Dio.
Features:
- Base URL and timeout configuration
- Request/response interceptors
- Authentication header management
- Automatic retry logic
- Error handling and conversion to custom exceptions
Usage Example:
final dioClient = DioClient();
// GET request
final response = await dioClient.get('/products');
// POST request
final response = await dioClient.post(
'/products',
data: {'name': 'Product Name', 'price': 29.99},
);
// With query parameters
final response = await dioClient.get(
'/products/search',
queryParameters: {'q': 'laptop'},
);
Methods:
get()- GET requestspost()- POST requestsput()- PUT requestsdelete()- DELETE requestspatch()- PATCH requestsdownload()- File downloadsupdateAuthToken()- Update authentication tokenremoveAuthToken()- Remove authentication tokenaddHeader()- Add custom headerremoveHeader()- Remove custom header
2. API Constants (/lib/core/constants/api_constants.dart)
Purpose: Centralized configuration for API endpoints, timeouts, and settings.
Configuration:
// Base URL
static const String baseUrl = 'https://api.retailpos.example.com';
static const String apiVersion = '/api/v1';
// Timeouts (in milliseconds)
static const int connectTimeout = 30000;
static const int receiveTimeout = 30000;
static const int sendTimeout = 30000;
// Retry configuration
static const int maxRetries = 3;
static const int retryDelay = 1000;
Endpoints:
// Products
ApiConstants.products // GET /products
ApiConstants.productById('123') // GET /products/123
ApiConstants.productsByCategory('cat1') // GET /products/category/cat1
ApiConstants.searchProducts // GET /products/search?q=query
ApiConstants.syncProducts // POST /products/sync
// Categories
ApiConstants.categories // GET /categories
ApiConstants.categoryById('123') // GET /categories/123
ApiConstants.syncCategories // POST /categories/sync
3. Network Info (/lib/core/network/network_info.dart)
Purpose: Check network connectivity status using connectivity_plus.
Features:
- Check current connectivity status
- Stream of connectivity changes
- Check connection type (WiFi, Mobile, etc.)
Usage Example:
final networkInfo = NetworkInfoImpl(Connectivity());
// Check if connected
final isConnected = await networkInfo.isConnected;
// Listen to connectivity changes
networkInfo.onConnectivityChanged.listen((isConnected) {
if (isConnected) {
print('Device is online');
} else {
print('Device is offline');
}
});
// Check connection type
final isWiFi = await networkInfo.isConnectedViaWiFi;
final isMobile = await networkInfo.isConnectedViaMobile;
4. API Interceptors (/lib/core/network/api_interceptor.dart)
Logging Interceptor
Logs all requests and responses for debugging.
REQUEST[GET] => PATH: /products
Headers: {Content-Type: application/json}
RESPONSE[200] => PATH: /products
Data: {...}
Authentication Interceptor
Automatically adds authentication headers to requests.
// Set auth token
authInterceptor.setAuthToken('your-jwt-token');
// All requests now include:
// Authorization: Bearer your-jwt-token
// Clear token
authInterceptor.clearAuthToken();
Error Interceptor
Converts HTTP status codes to custom exceptions.
Status Code Mapping:
400’BadRequestException401’UnauthorizedException403’ForbiddenException404’NotFoundException422’ValidationException429’RateLimitException500+’ServerException503’ServiceUnavailableException
Retry Interceptor
Automatically retries failed requests.
Retry Conditions:
- Connection timeout
- Send/receive timeout
- Connection errors
- HTTP 408, 429, 502, 503, 504
Retry Strategy:
- Max retries: 3 (configurable)
- Exponential backoff: delay * (retry_count + 1)
- Default delay: 1000ms
5. Custom Exceptions (/lib/core/errors/exceptions.dart)
Network Exceptions:
NoInternetException- No internet connectionTimeoutException- Request timeoutConnectionException- Connection failedNetworkException- General network error
Server Exceptions:
ServerException- Server error (500+)ServiceUnavailableException- Service unavailable (503)
Client Exceptions:
BadRequestException- Invalid request (400)UnauthorizedException- Authentication required (401)ForbiddenException- Access forbidden (403)NotFoundException- Resource not found (404)ValidationException- Validation failed (422)RateLimitException- Rate limit exceeded (429)
Cache Exceptions:
CacheException- Cache operation failedCacheNotFoundException- Data not found in cache
Data Exceptions:
DataParsingException- Failed to parse dataInvalidDataFormatException- Invalid data format
Business Exceptions:
OutOfStockException- Product out of stockInsufficientStockException- Insufficient stockTransactionException- Transaction failedPaymentException- Payment processing failed
6. Failure Classes (/lib/core/errors/failures.dart)
Failures are used in the domain/presentation layer to represent errors without throwing exceptions.
Network Failures:
NoInternetFailureConnectionFailureTimeoutFailureNetworkFailure
Server Failures:
ServerFailureServiceUnavailableFailure
Client Failures:
BadRequestFailureUnauthorizedFailureForbiddenFailureNotFoundFailureValidationFailureRateLimitFailure
Usage Pattern:
// In repository
try {
final products = await remoteDataSource.fetchProducts();
return Right(products);
} on NoInternetException {
return Left(NoInternetFailure());
} on ServerException catch (e) {
return Left(ServerFailure(e.message, e.statusCode));
} catch (e) {
return Left(NetworkFailure('Unexpected error: ${e.toString()}'));
}
7. Remote Data Sources
Product Remote Data Source (/lib/features/products/data/datasources/product_remote_datasource.dart)
Methods:
// Fetch all products
Future<List<ProductModel>> fetchProducts();
// Fetch single product
Future<ProductModel> fetchProductById(String id);
// Fetch products by category
Future<List<ProductModel>> fetchProductsByCategory(String categoryId);
// Search products
Future<List<ProductModel>> searchProducts(String query);
// Sync products
Future<void> syncProducts(List<ProductModel> products);
Usage Example:
final dataSource = ProductRemoteDataSourceImpl(dioClient);
// Fetch all products
final products = await dataSource.fetchProducts();
// Search products
final results = await dataSource.searchProducts('laptop');
// Fetch by category
final categoryProducts = await dataSource.fetchProductsByCategory('electronics');
Category Remote Data Source (/lib/features/categories/data/datasources/category_remote_datasource.dart)
Methods:
// Fetch all categories
Future<List<CategoryModel>> fetchCategories();
// Fetch single category
Future<CategoryModel> fetchCategoryById(String id);
// Sync categories
Future<void> syncCategories(List<CategoryModel> categories);
Setup & Installation
1. Dependencies
Already added to pubspec.yaml:
dependencies:
dio: ^5.7.0
connectivity_plus: ^6.1.1
equatable: ^2.0.7
get_it: ^8.0.4
2. Initialize Dependencies
In main.dart:
import 'package:flutter/material.dart';
import 'core/di/injection_container.dart' as di;
void main() async {
WidgetsFlutterBinding.ensureInitialized();
// Initialize dependencies
await di.initDependencies();
runApp(const MyApp());
}
3. Configure API Base URL
Update in /lib/core/constants/api_constants.dart:
static const String baseUrl = 'https://your-api-url.com';
4. Using Mock Data (Development)
Enable mock data in /lib/core/constants/api_constants.dart:
static const bool useMockData = true;
Usage Examples
Example 1: Fetch Products in a Repository
import 'package:dartz/dartz.dart';
import '../../../core/errors/exceptions.dart';
import '../../../core/errors/failures.dart';
import '../../domain/repositories/product_repository.dart';
import '../datasources/product_remote_datasource.dart';
import '../models/product_model.dart';
class ProductRepositoryImpl implements ProductRepository {
final ProductRemoteDataSource remoteDataSource;
final NetworkInfo networkInfo;
ProductRepositoryImpl({
required this.remoteDataSource,
required this.networkInfo,
});
@override
Future<Either<Failure, List<ProductModel>>> getProducts() async {
// Check network connectivity
if (!await networkInfo.isConnected) {
return Left(NoInternetFailure());
}
try {
final products = await remoteDataSource.fetchProducts();
return Right(products);
} on ServerException catch (e) {
return Left(ServerFailure(e.message, e.statusCode));
} on TimeoutException {
return Left(TimeoutFailure());
} on NetworkException catch (e) {
return Left(NetworkFailure(e.message, e.statusCode));
} catch (e) {
return Left(NetworkFailure('Unexpected error: ${e.toString()}'));
}
}
}
Example 2: Using in a Riverpod Provider
import 'package:riverpod_annotation/riverpod_annotation.dart';
import '../../../core/di/injection_container.dart';
import '../../data/datasources/product_remote_datasource.dart';
import '../../data/models/product_model.dart';
part 'products_provider.g.dart';
@riverpod
class Products extends _$Products {
@override
Future<List<ProductModel>> build() async {
final dataSource = sl<ProductRemoteDataSource>();
return await dataSource.fetchProducts();
}
Future<void> refresh() async {
state = const AsyncValue.loading();
state = await AsyncValue.guard(() async {
final dataSource = sl<ProductRemoteDataSource>();
return await dataSource.fetchProducts();
});
}
}
Example 3: Handling Errors in UI
// In your widget
Consumer(
builder: (context, ref, child) {
final productsAsync = ref.watch(productsProvider);
return productsAsync.when(
data: (products) => ProductGrid(products: products),
loading: () => const CircularProgressIndicator(),
error: (error, stack) {
// Handle different error types
if (error is NoInternetFailure) {
return ErrorWidget(
message: 'No internet connection',
onRetry: () => ref.refresh(productsProvider),
);
} else if (error is ServerFailure) {
return ErrorWidget(
message: 'Server error. Please try again later',
onRetry: () => ref.refresh(productsProvider),
);
}
return ErrorWidget(
message: 'An error occurred',
onRetry: () => ref.refresh(productsProvider),
);
},
);
},
);
Testing
Testing Network Connectivity
// Use mock implementation
final mockNetworkInfo = NetworkInfoMock(isConnected: false);
// Test offline scenario
final repository = ProductRepositoryImpl(
remoteDataSource: mockRemoteDataSource,
networkInfo: mockNetworkInfo,
);
final result = await repository.getProducts();
expect(result.isLeft(), true);
expect(result.fold((l) => l, (r) => null), isA<NoInternetFailure>());
Testing API Calls
// Use mock data source
final mockDataSource = ProductRemoteDataSourceMock();
final products = await mockDataSource.fetchProducts();
expect(products, isNotEmpty);
expect(products.first.name, 'Sample Product 1');
API Response Format
Expected JSON Response Formats
Products List
{
"products": [
{
"id": "1",
"name": "Product Name",
"description": "Product description",
"price": 29.99,
"imageUrl": "https://example.com/image.jpg",
"categoryId": "cat1",
"stockQuantity": 100,
"isAvailable": true,
"createdAt": "2024-01-01T00:00:00Z",
"updatedAt": "2024-01-01T00:00:00Z"
}
]
}
Or direct array:
[
{
"id": "1",
"name": "Product Name",
...
}
]
Single Product
{
"product": {
"id": "1",
"name": "Product Name",
...
}
}
Categories List
{
"categories": [
{
"id": "1",
"name": "Electronics",
"description": "Electronic devices",
"iconPath": "assets/icons/electronics.png",
"color": "#2196F3",
"productCount": 25,
"createdAt": "2024-01-01T00:00:00Z"
}
]
}
Error Response
{
"message": "Error message",
"error": "Detailed error",
"errors": {
"field1": ["Validation error 1"],
"field2": ["Validation error 2"]
}
}
Troubleshooting
Issue: Connection Timeout
Solution:
- Increase timeout in
api_constants.dart - Check network connectivity
- Verify API endpoint is accessible
Issue: SSL Certificate Error
Solution: In development, you can allow self-signed certificates:
(_dio.httpClientAdapter as IOHttpClientAdapter).createHttpClient = () {
final client = HttpClient();
client.badCertificateCallback = (cert, host, port) => true;
return client;
};
** Warning**: Never use this in production!
Issue: 401 Unauthorized
Solution:
- Check authentication token is set correctly
- Verify token hasn't expired
- Refresh token if necessary
Issue: Data Parsing Error
Solution:
- Verify API response format matches expected format
- Check JSON field names match model properties
- Add debug logging to see actual response
Best Practices
- Always check network connectivity before making API calls
- Use offline-first approach - read from cache first, sync in background
- Handle all error scenarios gracefully with user-friendly messages
- Implement proper retry logic for transient errors
- Log requests/responses in development, disable in production
- Use mock data sources for development and testing
- Implement request cancellation for expensive operations
- Cache API responses to improve performance
- Use proper timeout values based on expected response time
- Implement proper authentication token management
Next Steps
- Implement Repositories: Create repository implementations
- Add Use Cases: Define business logic in use cases
- Create Riverpod Providers: Wire up data layer with UI
- Implement Offline Sync: Add background sync logic
- Add Authentication: Implement OAuth/JWT authentication
- Implement Caching: Add response caching with Hive
- Add Pagination: Implement paginated API requests
- Error Tracking: Integrate error tracking (Sentry, Firebase Crashlytics)
Support & Resources
- Dio Documentation: https://pub.dev/packages/dio
- Connectivity Plus: https://pub.dev/packages/connectivity_plus
- GetIt Documentation: https://pub.dev/packages/get_it
- Riverpod Documentation: https://riverpod.dev
File Structure
lib/
core/
constants/
api_constants.dart API configuration
di/
injection_container.dart Dependency injection
errors/
exceptions.dart Custom exceptions
failures.dart Failure classes
network/
dio_client.dart Dio HTTP client
api_interceptor.dart Interceptors
network_info.dart Network connectivity
features/
products/
data/
datasources/
product_remote_datasource.dart
models/
product_model.dart
categories/
data/
datasources/
category_remote_datasource.dart
models/
category_model.dart
All API integration components are now complete and ready to use! <‰