Skip to main content

Networking Layer

MStore menggunakan Dio + Retrofit untuk HTTP networking dengan interceptor pipeline yang robust untuk handling authentication, retry, logging, dan error handling.

๐ŸŒ Arsitektur Networking

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚                    Application Layer                     โ”‚
โ”‚              (BLoC, Repository Interface)                โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                           โ†“
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚                   Repository Layer                       โ”‚
โ”‚         (ProductRepositoryRetrofit, AuthRepository)      โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                           โ†“
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚                    Retrofit API Layer                    โ”‚
โ”‚              (ProductApi, AuthApi, etc.)                 โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                           โ†“
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚                      Dio HTTP Client                     โ”‚
โ”‚                  (with Interceptors)                     โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                           โ†“
        โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
        โ”‚      Interceptor Pipeline            โ”‚
        โ”‚  1. CorrelationInterceptor           โ”‚
        โ”‚  2. HeadersInterceptor               โ”‚
        โ”‚  3. AuthInterceptor                  โ”‚
        โ”‚  4. RefreshTokenInterceptor          โ”‚
        โ”‚  5. RetryInterceptor                 โ”‚
        โ”‚  6. LoggingInterceptor               โ”‚
        โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                           โ†“
                    HTTP Request
                           โ†“
                      Backend API

๐Ÿ”ง Dio Configuration

Factory Function

File: lib/core/network/dio_client.dart
Dio createDio({
  required String baseUrl,
  Duration connect = const Duration(seconds: 10),
  Duration receive = const Duration(seconds: 20),
}) {
  final dio = Dio(
    BaseOptions(
      baseUrl: baseUrl,
      connectTimeout: connect,
      receiveTimeout: receive,
      sendTimeout: const Duration(seconds: 20),
      responseType: ResponseType.json,
      receiveDataWhenStatusError: true,
    ),
  );

  // JSON decode di isolate untuk performa
  dio.transformer = JsonIsolateTransformer();

  // HTTP/2 adapter untuk release, IO adapter untuk debug
  if (!kReleaseMode) {
    dio.httpClientAdapter = IOHttpClientAdapter();
  } else {
    dio.httpClientAdapter = Http2Adapter(ConnectionManager());
  }

  // Interceptor pipeline
  dio.interceptors.addAll([
    CorrelationInterceptor(),
    HeadersInterceptor(),
    AuthInterceptor(),
    RefreshTokenInterceptor(dio: dio),
    RetryInterceptor(dio: dio, maxRetries: 2),
    LoggingInterceptor(),
  ]);

  return dio;
}

Dependency Injection

File: lib/di/network_module.dart
@module
abstract class NetworkModule {
  @Named('baseUrl')
  String get baseUrl => API_BASE_URL;

  @LazySingleton()
  Dio dio(@Named('baseUrl') String baseUrl) => createDio(baseUrl: baseUrl);
}

๐Ÿ”Œ Interceptors

1. CorrelationInterceptor

Tujuan: Menambahkan correlation ID dan trace ID untuk request tracking.
class CorrelationInterceptor extends Interceptor {
  @override
  void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
    options.headers['X-Correlation-ID'] = Uuid().v4();
    options.headers['X-Trace-ID'] = Uuid().v4();
    handler.next(options);
  }
}
Headers yang ditambahkan:
  • X-Correlation-ID: Unique ID per request
  • X-Trace-ID: Trace ID untuk distributed tracing

2. HeadersInterceptor

Tujuan: Menambahkan common headers ke semua request.
class HeadersInterceptor extends Interceptor {
  @override
  void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
    options.headers.addAll({
      'Content-Type': 'application/json',
      'Accept': 'application/json',
      'X-Platform': Platform.operatingSystem,
      'X-App-Version': packageInfo.version,
      'X-Device-ID': deviceId,
    });
    handler.next(options);
  }
}
Headers:
  • Content-Type: application/json
  • Accept: application/json
  • X-Platform: ios/android/web/windows/macos
  • X-App-Version: App version dari package_info
  • X-Device-ID: Unique device identifier

3. AuthInterceptor

Tujuan: Menambahkan Authorization token ke request yang memerlukan autentikasi.
class AuthInterceptor extends Interceptor {
  @override
  void onRequest(RequestOptions options, RequestInterceptorHandler handler) async {
    // Skip untuk endpoint public
    if (_isPublicEndpoint(options.path)) {
      return handler.next(options);
    }

    final token = await AuthService().getAccessToken();
    if (token != null) {
      options.headers['Authorization'] = 'Bearer $token';
    }
    
    handler.next(options);
  }

  bool _isPublicEndpoint(String path) {
    return path.contains('/login') || 
           path.contains('/register') ||
           path.contains('/forgot-password');
  }
}

4. RefreshTokenInterceptor

Tujuan: Auto-refresh token saat access token expired (HTTP 401).
class RefreshTokenInterceptor extends QueuedInterceptor {
  final Dio dio;
  
  RefreshTokenInterceptor({required this.dio});

  @override
  void onError(DioException err, ErrorInterceptorHandler handler) async {
    // Hanya handle 401 Unauthorized
    if (err.response?.statusCode != 401) {
      return handler.next(err);
    }

    // Skip jika endpoint adalah refresh token itu sendiri
    if (err.requestOptions.path.contains('/refresh-token')) {
      return handler.next(err);
    }

    try {
      // Ambil refresh token
      final refreshToken = await AuthService().getRefreshToken();
      if (refreshToken == null) {
        throw Exception('No refresh token available');
      }

      // Request token baru
      final response = await dio.post(
        '/api/v1/auth/refresh-token',
        data: {'refreshToken': refreshToken},
      );

      final newAccessToken = response.data['accessToken'];
      final newRefreshToken = response.data['refreshToken'];

      // Simpan token baru
      await AuthService().saveTokens(
        accessToken: newAccessToken,
        refreshToken: newRefreshToken,
      );

      // Retry request original dengan token baru
      final opts = err.requestOptions;
      opts.headers['Authorization'] = 'Bearer $newAccessToken';
      
      final retryResponse = await dio.fetch(opts);
      return handler.resolve(retryResponse);
      
    } catch (e) {
      // Refresh gagal, logout user
      AppLog.e('auth', 'refresh_failed', e);
      await AuthService().logout();
      return handler.next(err);
    }
  }
}
Flow:
  1. Request gagal dengan 401
  2. Ambil refresh token dari storage
  3. Call /refresh-token endpoint
  4. Simpan token baru
  5. Retry request original
  6. Jika refresh gagal โ†’ logout user

5. RetryInterceptor

Tujuan: Retry request yang gagal karena network error.
class RetryInterceptor extends Interceptor {
  final Dio dio;
  final int maxRetries;
  
  RetryInterceptor({required this.dio, this.maxRetries = 2});

  @override
  void onError(DioException err, ErrorInterceptorHandler handler) async {
    // Hanya retry untuk network error
    if (!_shouldRetry(err)) {
      return handler.next(err);
    }

    final retryCount = err.requestOptions.extra['retryCount'] ?? 0;
    
    if (retryCount >= maxRetries) {
      AppLog.w('network', 'max_retry_reached', meta: {'count': retryCount});
      return handler.next(err);
    }

    // Exponential backoff
    final delay = Duration(milliseconds: 500 * (retryCount + 1));
    await Future.delayed(delay);

    // Increment retry count
    err.requestOptions.extra['retryCount'] = retryCount + 1;

    try {
      final response = await dio.fetch(err.requestOptions);
      return handler.resolve(response);
    } catch (e) {
      return handler.next(err);
    }
  }

  bool _shouldRetry(DioException err) {
    return err.type == DioExceptionType.connectionTimeout ||
           err.type == DioExceptionType.receiveTimeout ||
           err.type == DioExceptionType.connectionError;
  }
}
Retry Strategy:
  • Max retries: 2
  • Exponential backoff: 500ms, 1000ms, 1500ms
  • Hanya retry untuk: timeout dan connection error
  • Tidak retry untuk: 4xx, 5xx errors

6. LoggingInterceptor

Tujuan: Logging semua HTTP request/response untuk debugging.
class LoggingInterceptor extends Interceptor {
  @override
  void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
    AppLog.i('http', 'request', meta: {
      'method': options.method,
      'url': options.uri.toString(),
      'headers': options.headers,
      'data': options.data,
    });
    handler.next(options);
  }

  @override
  void onResponse(Response response, ResponseInterceptorHandler handler) {
    AppLog.i('http', 'response', meta: {
      'statusCode': response.statusCode,
      'url': response.requestOptions.uri.toString(),
      'data': response.data,
    });
    handler.next(response);
  }

  @override
  void onError(DioException err, ErrorInterceptorHandler handler) {
    AppLog.e('http', 'error', err, err.stackTrace, meta: {
      'url': err.requestOptions.uri.toString(),
      'statusCode': err.response?.statusCode,
      'message': err.message,
    });
    handler.next(err);
  }
}

๐Ÿ“ก Retrofit API

API Definition

File: lib/core/product/product_api.dart
@RestApi()
abstract class ProductApi {
  factory ProductApi(Dio dio, {String baseUrl}) = _ProductApi;

  @GET('/api/v1/products')
  Future<HttpResponse<List<Product>>> getProducts(
    @Query('page') int page,
    @Query('limit') int limit,
  );

  @GET('/api/v1/products/{id}')
  Future<HttpResponse<Product>> getProductById(
    @Path('id') String id,
  );

  @POST('/api/v1/products')
  Future<HttpResponse<Product>> createProduct(
    @Body() Map<String, dynamic> body,
  );

  @PUT('/api/v1/products/{id}')
  Future<HttpResponse<Product>> updateProduct(
    @Path('id') String id,
    @Body() Map<String, dynamic> body,
  );

  @DELETE('/api/v1/products/{id}')
  Future<HttpResponse<void>> deleteProduct(
    @Path('id') String id,
  );
}

Repository Implementation

File: lib/core/product/product_repository_retrofit.dart
class ProductRepositoryRetrofit implements ProductRepository {
  final Dio dio;
  final String baseUrl;
  late final ProductApi _api;

  ProductRepositoryRetrofit({
    required this.dio,
    required this.baseUrl,
  }) {
    _api = ProductApi(dio, baseUrl: baseUrl);
  }

  @override
  Future<Either<Failure, List<Product>>> getProducts() async {
    try {
      final response = await _api.getProducts(1, 100);
      return Right(response.data);
    } on DioException catch (e) {
      return Left(NetworkException.fromDioError(e));
    } catch (e) {
      return Left(UnexpectedFailure(e.toString()));
    }
  }

  @override
  Future<Either<Failure, Product>> getProductById(String id) async {
    try {
      final response = await _api.getProductById(id);
      return Right(response.data);
    } on DioException catch (e) {
      return Left(NetworkException.fromDioError(e));
    }
  }
}

โŒ Error Handling

NetworkException

File: lib/core/network/network_exceptions.dart
class NetworkException {
  static Failure fromDioError(DioException error) {
    switch (error.type) {
      case DioExceptionType.connectionTimeout:
      case DioExceptionType.sendTimeout:
      case DioExceptionType.receiveTimeout:
        return TimeoutFailure('Request timeout');

      case DioExceptionType.connectionError:
        return NetworkFailure('No internet connection');

      case DioExceptionType.badResponse:
        return _handleStatusCode(error.response?.statusCode);

      default:
        return UnexpectedFailure('Unexpected error occurred');
    }
  }

  static Failure _handleStatusCode(int? statusCode) {
    switch (statusCode) {
      case 401:
        return UnauthorizedFailure('Unauthorized access');
      case 403:
        return ForbiddenFailure('Access forbidden');
      case 404:
        return NotFoundFailure('Resource not found');
      case 422:
        return ValidationFailure('Validation error');
      case 500:
      case 502:
      case 503:
        return ServerFailure('Server error');
      default:
        return UnexpectedFailure('HTTP $statusCode');
    }
  }
}

Failure Types

abstract class Failure {
  final String message;
  Failure(this.message);
}

class NetworkFailure extends Failure {
  NetworkFailure(String message) : super(message);
}

class TimeoutFailure extends Failure {
  TimeoutFailure(String message) : super(message);
}

class UnauthorizedFailure extends Failure {
  UnauthorizedFailure(String message) : super(message);
}

class ServerFailure extends Failure {
  ServerFailure(String message) : super(message);
}

class ValidationFailure extends Failure {
  ValidationFailure(String message) : super(message);
}

class UnexpectedFailure extends Failure {
  UnexpectedFailure(String message) : super(message);
}

๐Ÿ”„ Request Flow Example

Complete Flow

// 1. User action
onPressed: () {
  context.read<ProductBloc>().add(LoadProducts());
}

// 2. BLoC handles event
Future<void> _onLoadProducts(
  LoadProducts event,
  Emitter<ProductState> emit,
) async {
  emit(ProductLoading());
  
  // 3. Call repository
  final result = await _productRepository.getProducts();
  
  // 4. Handle result
  result.fold(
    (failure) => emit(ProductError(failure.message)),
    (products) => emit(ProductLoaded(products)),
  );
}

// 5. Repository calls API
@override
Future<Either<Failure, List<Product>>> getProducts() async {
  try {
    // 6. Retrofit API call
    final response = await _api.getProducts(1, 100);
    
    // 7. Success
    return Right(response.data);
  } on DioException catch (e) {
    // 8. Error handling
    return Left(NetworkException.fromDioError(e));
  }
}

// Interceptor Pipeline (automatic):
// โ†’ CorrelationInterceptor: Add X-Correlation-ID
// โ†’ HeadersInterceptor: Add common headers
// โ†’ AuthInterceptor: Add Authorization token
// โ†’ [HTTP Request to Backend]
// โ†’ [HTTP Response from Backend]
// โ†’ LoggingInterceptor: Log response
// โ†’ If 401: RefreshTokenInterceptor triggers
// โ†’ If network error: RetryInterceptor retries

๐Ÿงช Testing

Mock Dio

class MockDio extends Mock implements Dio {}

void main() {
  late MockDio mockDio;
  late ProductRepositoryRetrofit repository;

  setUp(() {
    mockDio = MockDio();
    repository = ProductRepositoryRetrofit(
      dio: mockDio,
      baseUrl: 'https://api.test.com',
    );
  });

  test('getProducts returns list on success', () async {
    // Arrange
    when(() => mockDio.get(any())).thenAnswer(
      (_) async => Response(
        data: [{'id': '1', 'name': 'Product 1'}],
        statusCode: 200,
        requestOptions: RequestOptions(path: ''),
      ),
    );

    // Act
    final result = await repository.getProducts();

    // Assert
    expect(result.isRight(), true);
  });
}

๐Ÿ“Š Performance Optimization

1. JSON Parsing di Isolate

class JsonIsolateTransformer extends BackgroundTransformer {
  @override
  Future<String> transformRequest(RequestOptions options) async {
    if (options.data is Map || options.data is List) {
      return compute(jsonEncode, options.data);
    }
    return options.data.toString();
  }

  @override
  Future transformResponse(RequestOptions options, ResponseBody response) async {
    final responseBody = await response.stream.bytesToString();
    return compute(jsonDecode, responseBody);
  }
}

2. HTTP/2 Support

if (kReleaseMode) {
  dio.httpClientAdapter = Http2Adapter(
    ConnectionManager(
      idleTimeout: const Duration(seconds: 10),
      onClientCreate: (_, config) => config.onBadCertificate = (_) => true,
    ),
  );
}

3. Connection Pooling

Dio secara otomatis menggunakan connection pooling untuk reuse HTTP connections.

๐Ÿ” Security Best Practices

  1. HTTPS Only: Semua request menggunakan HTTPS
  2. Certificate Pinning (optional): Untuk production
  3. Token Storage: Encrypted di Isar database
  4. No Sensitive Data in Logs: Mask sensitive fields
  5. Request Timeout: Prevent hanging requests

๐Ÿ“ API Endpoints

Lihat dokumentasi lengkap di API Reference.

Next Steps


Best Practices:
  • โœ… Gunakan Retrofit untuk type-safe API calls
  • โœ… Handle semua error cases dengan Either pattern
  • โœ… Implement retry logic untuk network errors
  • โœ… Log semua requests untuk debugging
  • โœ… Auto-refresh token untuk seamless UX