Documentation Index
Fetch the complete documentation index at: https://docs-mstore.faisalaffan.com/llms.txt
Use this file to discover all available pages before exploring further.
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
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:
- Request gagal dengan 401
- Ambil refresh token dari storage
- Call
/refresh-token endpoint
- Simpan token baru
- Retry request original
- 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);
});
}
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
- HTTPS Only: Semua request menggunakan HTTPS
- Certificate Pinning (optional): Untuk production
- Token Storage: Encrypted di Isar database
- No Sensitive Data in Logs: Mask sensitive fields
- 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