How to update a get it instance in flutter? - flutter

I am using the getIt package to create instances in my application.
instance.registerLazySingleton<DioFactory>(() => DioFactory(instance()));
// app service client
final dio = await instance<DioFactory>().getDio();
instance.registerLazySingleton<AppServiceClient>(() => AppServiceClient(dio));
Above code is for initialising the instances.
The getDio() function:
Future<Dio> getDio() async {
Dio dio = Dio();
int _timeOut = 60 * 1000; // 1 min
String language = await _appPreferences.getAppLanguage();
Map<String, String> headers = {
CONTENT_TYPE: APPLICATION_JSON,
ACCEPT: APPLICATION_JSON,
AUTHORIZATION: Constants.token,
DEFAULT_LANGUAGE: language
};
dio.options = BaseOptions(
baseUrl: Constants.baseUrl,
connectTimeout: _timeOut,
receiveTimeout: _timeOut,
headers: headers);
if (kReleaseMode) {
} else {
dio.interceptors.add(PrettyDioLogger(
requestHeader: true, requestBody: true, responseHeader: true));
}
return dio;
}
When I build my application for the first time the Constants.token has a blank value.
But in the middle of the application I wish to add a value into it. I am able to change that value but when I see the logs from dio logger it still displays the empty string in the "Authorisation" field.
How do I update my instance so that I can change my token value for my API requests?

you have to register your dio with registerFactory to getit. In your case you are registering as singleton, that's why it is giving you same instance

First you should create an interceptor that call TokenInterceptor with override onRequest method. Inside onRequest method you can modify your headers like below:
https://i.stack.imgur.com/T6PFR.png
And final step you only add to your dio interceptor like that:
dio.interceptors.add(TokenInterceptor());

final getIt = GetIt.instance;
void setupSingleton(){
getIt.allowReassignment=true;
}
//this should call first and then it will work

Related

How to set timeout for Api call using retrofit in flutter?

Where we can set timeout in below code?
As you can see I am using retrofit for api call.
Dio Object
class DioObject{
static Dio getDio(){
debugPrint("Bearer:- ${PrefHelper().pref?.getString(PrefHelper.AUTHORIZATION)}");
final dio = Dio(); // Provide a dio instance
dio.options.headers["Authorization"] =
"Bearer ${PrefHelper().pref?.getString(PrefHelper.AUTHORIZATION)}"; // config your dio headers globally
dio.options.headers["Content-Type"] =
"application/json;charset=UTF-8"; // config your dio headers globally
return dio;
}
}
Api call
final client = RestClient(DioObject.getDio());
var response = await client.xyz();
Rest API
#RestApi(baseUrl: "*****/api")
abstract class RestClient {
factory RestClient(Dio dio, {String baseUrl}) = _RestClient;
#GET("/UserAccessPoints/")
Future<CommonResponse> xyz();
}
You can do it the same way you added your custom headers, or doing it all at once with a BaseOptions object
final dio = Dio();
//Dio Options
dio.options = BaseOptions(
contentType: 'application/json',
connectTimeout: 4000,
sendTimeout: 4000,
receiveTimeout: 10000,
headers : ...
);

Dio Client: if request to protected route fails (401 code), then refresh the token and try again. Struggling to create

I am trying to create a custom ApiClient class that I can inject as a dependency (with get_it package) to be used in the data layer of my application. In order not to worry about access tokens throughout the presentation/application/domain layers of my app, I'd like to have a field, accessToken, that keeps track of the accessToken inside the ApiClient (singleton) class.
The ApiClient class would be used all throughout my data layer to handle requests to my server for data. It should have a method that allows me to add my own requests to it for unique routes. Then, if those routes require access tokens, it will add the accessToken field from the class along with the request. If that access token is invalid (expired/tampered with), then I would use the refresh token from the device's storage and send a request to the server to get a new access token, then try the original request again. It would "retry" the request at maximum once. Then, if there's still an error, it just returns that to be handled.
I am really struggling with how to implement this. My current attempt is below. Any help would be amazing!
class ApiClient {
final String baseUrl;
final Dio dio;
final NetworkInfo networkInfo;
final FlutterSecureStorage secureStorage;
ApiClient(
{required this.baseUrl,
required this.dio,
required this.networkInfo,
required this.secureStorage}) {
dio.interceptors.add(RefreshInvalidTokenInterceptor(networkInfo, dio, secureStorage));
}
}
class RefreshInvalidTokenInterceptor extends QueuedInterceptor {
final NetworkInfo networkInfo;
final Dio dio;
final FlutterSecureStorage secureStorage;
String? accessToken;
RefreshInvalidTokenInterceptor(this.networkInfo, this.dio, this.secureStorage);
#override
Future onError(DioError err, ErrorInterceptorHandler handler) async {
if (_shouldRetry(err) && await networkInfo.isConnected) {
try {
// access token request (using refresh token from flutter_secure_storage)
final refreshToken = await secureStorage.read(key: "refreshToken");
final response = await dio.post(
"$kDomain/api/user/token",
queryParameters: {"token": refreshToken},
);
accessToken = response.data["accessToken"];
return err;
} on DioError catch (e) {
handler.next(e);
} catch (e) {
handler.next(err);
}
} else {
handler.next(err);
}
}
bool _shouldRetry(DioError err) =>
(err.response!.statusCode == 403 || err.response!.statusCode == 401);
}
There are similar questions online, but none seem to answer my question! :)
EDIT: I've gotten a working solution (almost), with just 1 error. This works (except in the function retryRequest() I'm hardcoding the request to be a post request):
<imports removed for simplicity>
class ApiClient {
final Dio dio;
final NetworkInfo networkInfo;
final FlutterSecureStorage secureStorage;
String? accessToken;
ApiClient({
required this.dio,
required this.networkInfo,
required this.secureStorage,
}) {
dio.options = BaseOptions(
connectTimeout: 5000,
receiveTimeout: 3000,
receiveDataWhenStatusError: true,
followRedirects: true,
headers: {"content-Type": "application/json"},
);
dio.interceptors.add(QueuedInterceptorsWrapper(
//! ON REQUEST
onRequest: (options, handler) {
handler.next(options);
},
//! ON RESPONSE
onResponse: (response, handler) {
print("onResponse...");
handler.next(response);
},
//! ON ERROR
onError: (error, handler) async {
print("onError...");
if (tokenInvalid(error)) {
print("token invalid: retrying");
print("header before: ${dio.options.headers}");
await getAccessTokenAndSetToHeader(dio);
print("header after: ${dio.options.headers}");
final response = await retryRequest(error, handler);
handler.resolve(response);
print("here-1");
} else {
handler.reject(error);
}
print("here-2");
print("here-3");
},
));
}
Future<String?> getRefreshToken() async => await secureStorage.read(key: "refreshToken");
Future<void> getAccessTokenAndSetToHeader(Dio dio) async {
final refreshToken = await secureStorage.read(key: "refreshToken");
if (refreshToken == null || refreshToken.isEmpty) {
print("NO REFRESH TOKEN ERROR; LOGOUT!!!");
throw ServerException();
} else {
final response = await dio.post(
"$kDomain/api/user/token",
data: {"token": refreshToken},
);
dio.options.headers["authorization"] = "Bearer ${response.data["accessToken"]}";
}
}
// This function has the hardcoded post
Future<Response> retryRequest(DioError error, ErrorInterceptorHandler handler) async {
print("retry called, headers: ${dio.options.headers}");
final retryResponse = await dio.post(error.requestOptions.path);
print("retry results: $retryResponse");
return retryResponse;
}
bool tokenInvalid(DioError error) =>
error.response?.statusCode == 403 || error.response?.statusCode == 401;
Future<void> refreshToken() async {}
bool validStatusCode(Response response) =>
response.statusCode == 200 || response.statusCode == 201;
}
However, if I change the hardcoded post request to:
final retryResponse =
await dio.request(error.requestOptions.path, data: error.requestOptions.data);
the code no longer works... anyone know why? Having it dynamic based on whatever the failed request was, lets me re-use this class.
package:dio already include the BaseOptions which you can use to add some basic configuration like the baseUrl.
After that, you could use interceptors to add the accessToken to every request. To do this depending on your state management solution you can update the accessToken when the user authentication state changes.
And finally regarding the token refresh you can checkout package:fresh_dio.
Figured it out! (code + how to use below)
Here is my entire ApiClient class (imports hidden for simplicity). It acts as an HTTP client using dio:
class ApiClient {
final Dio dio;
final NetworkInfo networkInfo;
final FlutterSecureStorage secureStorage;
String? accessToken;
/// The base options for all requests with this Dio client.
final BaseOptions baseOptions = BaseOptions(
connectTimeout: 5000,
receiveTimeout: 3000,
receiveDataWhenStatusError: true,
followRedirects: true,
headers: {"content-Type": "application/json"},
baseUrl: kDomain, // Domain constant (base path).
);
/// Is the current access token valid? Checks if it's null, empty, or expired.
bool get validToken {
if (accessToken == null || accessToken!.isEmpty || Jwt.isExpired(accessToken!)) return false;
return true;
}
ApiClient({
required this.dio,
required this.networkInfo,
required this.secureStorage,
}) {
dio.options = baseOptions;
dio.interceptors.add(QueuedInterceptorsWrapper(
// Runs before a request happens. If there's no valid access token, it'll
// get a new one before running the request.
onRequest: (options, handler) async {
if (!validToken) {
await getAndSetAccessTokenVariable(dio);
}
setHeader(options);
handler.next(options);
},
// Runs on an error. If this error is a token error (401 or 403), then the access token
// is refreshed and the request is re-run.
onError: (error, handler) async {
if (tokenInvalidResponse(error)) {
await refreshAndRedoRequest(error, handler);
} else {
// Other error occurs (non-token issue).
handler.reject(error);
}
},
));
}
/// Sets the current [accessToken] to request header.
void setHeader(RequestOptions options) =>
options.headers["authorization"] = "Bearer $accessToken";
/// Refreshes access token, sets it to header, and resolves cloned request of the original.
Future<void> refreshAndRedoRequest(DioError error, ErrorInterceptorHandler handler) async {
await getAndSetAccessTokenVariable(dio);
setHeader(error.requestOptions);
handler.resolve(await dio.post(error.requestOptions.path,
data: error.requestOptions.data, options: Options(method: error.requestOptions.method)));
}
/// Gets new access token using the device's refresh token and sets it to [accessToken] class field.
///
/// If the refresh token from the device's storage is null or empty, an [EmptyTokenException] is thrown.
/// This should be handled with care. This means the user has somehow been logged out!
Future<void> getAndSetAccessTokenVariable(Dio dio) async {
final refreshToken = await secureStorage.read(key: "refreshToken");
if (refreshToken == null || refreshToken.isEmpty) {
// User is no longer logged in!
throw EmptyTokenException();
} else {
// New DIO instance so it doesn't get blocked by QueuedInterceptorsWrapper.
// Refreshes token from endpoint.
try {
final response = await Dio(baseOptions).post(
"/api/user/token",
data: {"token": refreshToken},
);
// If refresh fails, throw a custom exception.
if (!validStatusCode(response)) {
throw ServerException();
}
accessToken = response.data["accessToken"];
} on DioError catch (e) {
// Based on the different dio errors, throw custom exception classes.
switch (e.type) {
case DioErrorType.sendTimeout:
throw ConnectionException();
case DioErrorType.connectTimeout:
throw ConnectionException();
case DioErrorType.receiveTimeout:
throw ConnectionException();
case DioErrorType.response:
throw ServerException();
default:
throw ServerException();
}
}
}
}
bool tokenInvalidResponse(DioError error) =>
error.response?.statusCode == 403 || error.response?.statusCode == 401;
bool validStatusCode(Response response) =>
response.statusCode == 200 || response.statusCode == 201;
}
It should be injected as a singleton to your project so there's one instance of it (for the sake of keeping the state of its accessToken field). I used get_it like so:
// Registers the custom ApiClient class.
sl.registerLazySingleton(() => ApiClient(dio: sl(), networkInfo: sl(), secureStorage: sl()));
Then, inside your data layer (or wherever you call APIs from), you can use it by passing it through the constructor:
class MyDatasource implements IMyDatasource {
final ApiClient apiClient;
late Dio api;
FeedDatasource({required this.client, required this.apiClient}) {
api = apiClient.dio;
}
// Logic for your class here.
}
I simplified it to api so I wouldn't have to go apiClient.dio... every call (optional).
Then, you can use it in one of your class' methods like so:
#override
Future<List<SomeData>> fetchSomeDataFromApi() async {
try {
final response = await api.post("/api/data/whatYouWant");
throw ServerException();
} catch (e) {
throw ServerException();
}
}
Now, for this request, if your class has a valid access token (non-null, non-empty, non-expired), it will call normally. However, if your token isn't valid, it'll refresh it first, then proceed with your call. Even if the call fails after the token originally passed the validation check (token somehow expires during the call for example), it will still be refreshed, and the call re-executed.
Note: I use a lot of custom exceptions, this is optional.
Hopefully this helps someone else!

Dio Cancel current running API before starting a new API request

I am using DIO package for API request but the issue is that when I request for another API while the first API is still in progress.
It doesn't cancel the first request. Both the APIs run simultaneously which is not the desired in my app scenario.
class DioClient {
static BaseOptions options = BaseOptions(baseUrl: baseUrl);
Dio _dio = Dio(options);
Future<dynamic> postFormData(
{dynamic data, String url, dynamic header}) async {
final data1 = data;
var formData = FormData.fromMap(data1);
try {
var response = await _dio.post(url,
options: Options(headers: header), data: formData);
return response.data;
} catch (e) {
throw e;
}}}
If you want to cancel the API request call then you need to use the cancel token provided by DIO.
You need to pass cancel token in dio request when you make other API call use that cancel token to cancel the API request
Here is the code
class DioClient {
static BaseOptions options = BaseOptions(baseUrl: baseUrl);
//Here is line you need
CancelToken cancelToken=CancelToken();
Dio _dio = Dio(options);
Future<dynamic> postFormData(
{dynamic data, String url, dynamic header}) async {
final data1 = data;
var formData = FormData.fromMap(data1);
try {
//pass cancel token here
var response = await _dio.post(url,
options: Options(headers: header), data: formData,cancelToken: cancelToken);
return response.data;
} catch (e) {
throw e;
}}}
And use that cancelToken to cancel the API request when you call another API first you cancel the previous request.
cancelToken.cancel();
Enjoy!

Flutter cache mechanism for Dio retrofit sub library

i my application i'm using Dio's Retrofit sub library, and inside that i'm not sure how can i define cache mechanism for that,
i found this line in Retrofit sample code:
#GET("")
Future<String> testCustomOptions(#DioOptions() Options options);
now how can i define cache on this http request?
Retrofit documentation:
void main(List<String> args) {
final dio = Dio(); // Provide a dio instance
dio.options.headers["Demo-Header"] = "demo header"; // config your dio headers globally
final client = RestClient(dio);
client.getTasks().then((it) => logger.i(it));
dio-http-cache documentation:
QuickStart
Add a dio-http-cache interceptor in Dio :
dio.interceptors.add(DioCacheManager(CacheConfig(baseUrl: "http://www.google.com")).interceptor);
Set maxAge for a request :
Dio().get(
"http://www.google.com",
options: buildCacheOptions(Duration(days: 7)),
);
my full implemented code:
Provider(
create: (_) => MyApis.create(),
),
abstract class MyApis{
factory MyApis(Dio dio, {String baseUrl}) = _MyApis;
#GET("/login")
Future<HttpResponse<PageInformation>> login(#DioOptions() Options options);
static MyApis create() {
final dio = Dio();
dio.options.headers['Content-Type'] = 'application/json';
dio.options.receiveTimeout = 60000;
dio.options.connectTimeout = 120000;
return _MyApis(dio);
}
}
Add Parameter in your API Add point Call
Future<HttpResponse<PageInformation>> login(#DioOptions() Options options);
Add Interceptor in Dio
dio.interceptors.add(DioCacheManager(CacheConfig(baseUrl: 'YOUR BASE URL')).interceptor);
Create an object for Endpoint argument
Options options = buildCacheOptions(Duration(days: 10),forceRefresh: true);
Call your API End point
myapi.login(options)

Global configuration (interceptor) for dio in Flutter

First time with Flutter. I'm using dio to send HTTP requests, and I have to add a header to all requests, which I do with an interceptor, like this:
Dio dio = new Dio();
dio.interceptors.add(InterceptorsWrapper(
onRequest:(RequestOptions options) async {
options.headers["X-Requested-With"] = "XMLHttpRequest";
})
);
It works in main.dart, but if I want to import another class like MyHomePage.dart and do HTTP requests there, I'd have to redefine the interceptor in that class too.
How can I implement this interceptor for my whole application without adding it in every .dart file?
Create a function that houses the DIO and then call it where needed
Dio getDio() {
Dio dio = new Dio();
dio.interceptors.add(InterceptorsWrapper(onRequest: (RequestOptions options) async {
options.headers["X-Requested-With"] = "XMLHttpRequest";
}));
return dio;
}
This worked good for me, without interceptors, just create a class and use it in your app.
import 'package:dio/dio.dart';
import '../helpers/api_url.dart';
class dioClient {
Dio dio = Dio();
static Dio simpleDio() {
return Dio(BaseOptions(
baseUrl: apiUrl(),
headers: {'Content-Type': 'application/json; charset=UTF-8'}));
}
static Dio dioWithCookie(String cookie) {
return Dio(BaseOptions(baseUrl: apiUrl(), headers: {
'Content-Type': 'application/json; charset=UTF-8',
'Cookie': cookie
}));
}
}