Skip to content

Instantly share code, notes, and snippets.

@Sp4Rx
Created January 5, 2023 11:53
Show Gist options
  • Save Sp4Rx/18f7962589af09bbbb95e05d1577473f to your computer and use it in GitHub Desktop.
Save Sp4Rx/18f7962589af09bbbb95e05d1577473f to your computer and use it in GitHub Desktop.
Flutter Api Client with dio
import 'dart:developer';
import 'dart:io';
import 'package:dio/dio.dart';
import 'package:flutter/cupertino.dart';
import 'package:get/get.dart' as getX;
import 'package:provider/provider.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:tranzact/revamp/core/api_client/api_exception.dart';
import 'package:tranzact/revamp/core/api_client/api_result.dart';
import 'package:tranzact/revamp/core/auth/auth_repository.dart';
import 'package:tranzact/revamp/core/utils/utils.dart';
import 'package:tranzact/revamp/modules/Authentication/repository/google_signin_api.dart';
import 'package:tranzact/revamp/modules/onboarding/controller/onboarding_controller.dart';
import 'package:tranzact/routes/routes_services.dart';
import 'package:tranzact/tracking/tracking_base.dart';
import 'package:tranzact/utils_and_services/crashlytics_helper.dart';
import 'package:tranzact/utils_and_services/globale_constants.dart';
import 'package:tranzact/utils_and_services/performance/dio_firebase_performance.dart';
import 'header_interceptor.dart';
typedef JsonMap = Map<String, dynamic>;
class ApiClient {
static final ApiClient _instance = ApiClient.internal();
static late Dio _dio;
static ApiResult apiResult = ApiResult();
ApiClient.internal() {
_dio = Dio(
BaseOptions(
baseUrl: Const.apiUrl,
),
)
..interceptors.add(AuthInterceptor())
..interceptors.add(DioFirebasePerformanceInterceptor())
..interceptors.add(LogInterceptor(responseBody: true, logPrint: _log));
}
factory ApiClient() => _instance;
CancelToken _cancelToken = CancelToken();
Future get(url, {Map<String, dynamic>? headers, bool? v2}) async {
try {
Response response = await _dio.get(
url,
options: Options(headers: headers),
cancelToken: _cancelToken,
);
if (v2 == true) {
return response.data;
}
return response;
} catch (error) {
return _handleError(url, error);
}
}
Future<dynamic> post(url, {required JsonMap body, Map<String, dynamic>? headers, bool? v2}) async {
try {
Response response = await _dio.post(
url,
data: body,
options: Options(headers: headers),
cancelToken: _cancelToken,
);
if (v2 == true) {
return response.data;
}
return response;
} catch (error) {
return _handleError(url, error);
}
}
Future update(
String url, {
required JsonMap body,
Map<String, dynamic>? headers,
}) async {
try {
Response response = await _dio.put(
url,
data: body,
options: Options(headers: headers),
cancelToken: _cancelToken,
);
return response.data;
} catch (error) {
return _handleError(url, error);
}
}
Future delete(
String url, {
required JsonMap body,
Map<String, dynamic>? header,
}) async {
try {
Response response = await _dio.delete(
url,
data: body,
options: Options(headers: header),
cancelToken: _cancelToken,
);
return response.data;
} catch (error) {
return _handleError(url, error);
}
}
Future uploadMediaRequest(
String url, {
required imagePath,
required filename,
}) async {
Map<String, dynamic> headerWithToken = {
'accept': 'application/json',
'Content-Type': 'application/json',
"authorization": "Bearer ${AuthRepository.accessToken}"
};
FormData formData = FormData.fromMap({
"company_image": await MultipartFile.fromFile(
imagePath,
filename: filename,
),
"image_of": "own_company"
});
try {
Response response = await _dio.put(
url,
data: formData,
options: Options(headers: headerWithToken),
cancelToken: _cancelToken,
);
return response.data;
} catch (error) {
return _handleError(url, error);
}
}
Future<Map<String, dynamic>> _handleError(String path, Object error) {
if (error is DioError) {
final method = error.requestOptions.method;
final response = error.response;
//TODO: Use this stream to show errors on UI
apiResult.setStatusCode(response?.statusCode);
final data = response?.data;
int? statusCode = response?.statusCode;
if (statusCode == 401) {
//TODO: Need to add this logout method in the Auth class
logoutUser();
} else {}
Utils.closeFullScreenTranzactLoader();
if (error.type == DioErrorType.connectTimeout ||
error.type == DioErrorType.sendTimeout ||
error.type == DioErrorType.receiveTimeout) {
statusCode = HttpStatus.requestTimeout; //Set the error code to 408 in case of timeout
}
throw ApiException(
path: path,
message: 'received server error $statusCode while $method data',
response: data.toString(),
statusCode: statusCode,
method: method,
);
} else {
int errorCode = 0; //We will send a default error code as 0
throw ApiException(
path: path,
message: 'received server error $errorCode',
response: error.toString(),
statusCode: errorCode,
method: '',
);
}
}
logoutUser() async {
if (AuthRepository.isLoggedIn) {
logAnalytics(
event_name: "auth_error",
event_label: "",
sendTo: [AnalyticsType.all],
module: "profile",
);
_cancelToken.cancel('Logged out');
await AuthRepository.instance.signOut();
final SharedPreferences preferences = await SharedPreferences.getInstance();
Provider.of<OnboardingController>(getX.Get.context!, listen: false).signOut();
await preferences.clear();
await GoogleSignInApi.logout();
CrashHelper.instance.clearUserIdentifier();
// Assign a new cancel token as dio will not process any new reques
// with the old token.
_cancelToken = CancelToken();
Navigator.of(getX.Get.context!).pushAndRemoveUntil(AppRouter.mainWelcomeScreen(), (Route<dynamic> route) => false);
getX.Get.deleteAll();
}
}
void _log(Object object) {
log("$object");
}
}
class ApiException implements Exception {
final String message;
final String path;
final int? statusCode;
final String? method;
final String? userId;
final dynamic response;
ApiException({
required this.message,
required this.path,
this.userId,
this.method,
this.statusCode,
this.response,
});
@override
String toString() {
return 'ApiException{message: $message, '
'path: $path, '
'statusCode: $statusCode, '
'method: $method, '
'userId: $userId, '
'response: $response}';
}
}
import 'dart:async';
class ApiResult {
StreamController<int> statusCode = StreamController();
static final ApiResult _instance = ApiResult.internal();
ApiResult.internal();
factory ApiResult() => _instance;
setStatusCode(int? statusCodeResult) {
if (!statusCode.isClosed) {
statusCode.add(statusCodeResult ?? 0);
}
}
StreamController<int> listenStatusCode() {
statusCode = StreamController();
return statusCode;
}
closeStream() {
statusCode.close();
}
}
import 'dart:developer';
import 'package:dio/dio.dart';
import 'package:tranzact/revamp/core/auth/auth_apis.dart';
import 'package:tranzact/revamp/core/auth/auth_repository.dart';
import 'package:tranzact/revamp/core/auth/auth_util.dart';
import 'package:tranzact/revamp/core/auth/model/token/token.dart';
import 'package:tranzact/utils_and_services/globale_constants.dart';
///1. Create default header
///2. Add token if exists
///3. Add other header params if passed
///4. Refresh the token if [_authErrorCode]
class AuthInterceptor extends QueuedInterceptor {
/// Dio instance to fetch refresh token.
/// Main dio instance will be blocked by [AuthInterceptor].
/// That's why creating a new dio instance
final Dio _alternateDio = Dio(BaseOptions(
baseUrl: Const.apiUrl,
))
..interceptors.add(LogInterceptor(
responseBody: true,
logPrint: (Object object) {
log("$object");
}));
static const int _authErrorCode = 401;
@override
void onRequest(
RequestOptions options, RequestInterceptorHandler handler) async {
try {
if (await _hasRequestTokenExpired()) {
options = options.copyWith(
headers: _createHeader(options.headers),
);
handler.next(options);
} else {
_sendAuthError(handler, options);
}
} catch (_) {
_sendAuthError(handler, options);
}
}
@override
void onError(DioError err, ErrorInterceptorHandler handler) async {
if (err.response?.statusCode != _authErrorCode) {
return super.onError(err, handler);
}
try {
// In case of [_authErrorCode] trying to get new token.
await _getAndStoreNewToken();
// Retrying the existing request.
final requestOptions = err.requestOptions.copyWith(
headers: _createHeader(
err.requestOptions.headers,
),
);
// Sending back the response with new auth token
final Response response = await _alternateDio.fetch(requestOptions);
handler.resolve(response);
} catch (_) {}
super.onError(err, handler);
}
Map<String, dynamic> _createHeader(Map<String, dynamic>? headers) {
//Create default header
Map<String, dynamic> tempHeader = {
'accept': 'application/json',
'Content-Type': 'application/json',
};
//Add other header params if passed
if (headers != null) {
tempHeader.addAll(headers);
}
//Add token if exists
if (AuthRepository.accessToken != null) {
tempHeader['Authorization'] = "Bearer ${AuthRepository.accessToken}";
} else {
tempHeader.remove('Authorization');
}
return tempHeader;
}
Future<void> _getAndStoreNewToken() async {
try {
final refreshToken = AuthRepository.refreshToken;
final response = await _alternateDio.post(
AuthApis.getRefreshToken,
data: {
"refresh_token": refreshToken,
},
);
final Token token = Token.fromJson(response.data);
AuthRepository.instance.setAccessToken(token.accessToken);
AuthRepository.instance.setRefreshToken(token.refreshToken);
} catch (_) {
rethrow;
}
}
///Checks validity of refresh token and access token
///Also updates the access token if it is expired.
///<br>
///Returns false if refreshToken is expired
///Returns true if all tokens are valid.
///Returns true if both tokens are null because on this case no auth is required.
///Returns true if token is refreshed
Future<bool> _hasRequestTokenExpired() async {
final refreshToken = AuthRepository.refreshToken;
final accessToken = AuthRepository.accessToken;
if (refreshToken == null && accessToken == null) {
return true;
}
if (refreshToken == null || accessToken == null) {
return false;
}
if (!AuthUtil.hasJwtTokenExpired(refreshToken)) {
if (!AuthUtil.hasJwtTokenExpired(accessToken)) {
return true;
} else {
await _getAndStoreNewToken();
return true;
}
}
return false;
}
/// All tokens have expired. Throwing [DioError] with
/// status code [_authErrorCode] so that the app logs out.
void _sendAuthError(
RequestInterceptorHandler handler,
RequestOptions options,
) {
handler.reject(
DioError(
requestOptions: options,
response: Response(
requestOptions: options,
statusCode: _authErrorCode,
),
),
);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment