Created
January 5, 2023 11:53
-
-
Save Sp4Rx/18f7962589af09bbbb95e05d1577473f to your computer and use it in GitHub Desktop.
Flutter Api Client with dio
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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"); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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}'; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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