Files
retail/docs/API_INTEGRATION_GUIDE.md
Phuoc Nguyen b94c158004 runable
2025-10-10 16:38:07 +07:00

17 KiB
Raw Permalink Blame History

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 requests
  • post() - POST requests
  • put() - PUT requests
  • delete() - DELETE requests
  • patch() - PATCH requests
  • download() - File downloads
  • updateAuthToken() - Update authentication token
  • removeAuthToken() - Remove authentication token
  • addHeader() - Add custom header
  • removeHeader() - 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 BadRequestException
  • 401 UnauthorizedException
  • 403 ForbiddenException
  • 404 NotFoundException
  • 422 ValidationException
  • 429 RateLimitException
  • 500+ ServerException
  • 503 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 connection
  • TimeoutException - Request timeout
  • ConnectionException - Connection failed
  • NetworkException - 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 failed
  • CacheNotFoundException - Data not found in cache

Data Exceptions:

  • DataParsingException - Failed to parse data
  • InvalidDataFormatException - Invalid data format

Business Exceptions:

  • OutOfStockException - Product out of stock
  • InsufficientStockException - Insufficient stock
  • TransactionException - Transaction failed
  • PaymentException - 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:

  • NoInternetFailure
  • ConnectionFailure
  • TimeoutFailure
  • NetworkFailure

Server Failures:

  • ServerFailure
  • ServiceUnavailableFailure

Client Failures:

  • BadRequestFailure
  • UnauthorizedFailure
  • ForbiddenFailure
  • NotFoundFailure
  • ValidationFailure
  • RateLimitFailure

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

  1. Always check network connectivity before making API calls
  2. Use offline-first approach - read from cache first, sync in background
  3. Handle all error scenarios gracefully with user-friendly messages
  4. Implement proper retry logic for transient errors
  5. Log requests/responses in development, disable in production
  6. Use mock data sources for development and testing
  7. Implement request cancellation for expensive operations
  8. Cache API responses to improve performance
  9. Use proper timeout values based on expected response time
  10. Implement proper authentication token management

Next Steps

  1. Implement Repositories: Create repository implementations
  2. Add Use Cases: Define business logic in use cases
  3. Create Riverpod Providers: Wire up data layer with UI
  4. Implement Offline Sync: Add background sync logic
  5. Add Authentication: Implement OAuth/JWT authentication
  6. Implement Caching: Add response caching with Hive
  7. Add Pagination: Implement paginated API requests
  8. Error Tracking: Integrate error tracking (Sentry, Firebase Crashlytics)

Support & Resources


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! <‰