Flutter: How to refresh token when token expires with ferry (graphql) client? - flutter

How to retrieve a new token with a refresh token in flutter in a ferry (graphql) client?
The response after a mutation looks like this:
{
"data": {
"auth_login": {
"access_token": "ey...",
"refresh_token": "Ua...",
"expires": 900000
}
}
}
I tried to accomplish it with fresh_graphql, but it does not work. The authenticationStatus is always unauthenticated but the token was always legit.
Implementation:
import 'dart:math';
import 'package:ferry/ferry.dart';
import 'package:ferry_hive_store/ferry_hive_store.dart';
import 'package:fresh_graphql/fresh_graphql.dart';
import 'package:gql_http_link/gql_http_link.dart';
import 'package:hive/hive.dart';
Future<Client> initClient(String? accessToken, String? refreshToken) async {
Hive.init('hive_data');
final box = await Hive.openBox<Map<String, dynamic>>('graphql');
await box.clear();
final store = HiveStore(box);
final cache = Cache(store: store);
final freshLink = await setFreshLink(accessToken ?? '', refreshToken);
final link = Link.from(
[freshLink, HttpLink('https://.../graphql/')]);
final client = Client(
link: link,
cache: cache,
);
return client;
}
Future<FreshLink> setFreshLink(String accessToken, String? refreshToken) async {
final freshLink = FreshLink<dynamic>(
tokenStorage: InMemoryTokenStorage<dynamic>(),
refreshToken: (dynamic token, client) async {
print('refreshing token!');
await Future<void>.delayed(const Duration(seconds: 1));
if (Random().nextInt(1) == 0) {
throw RevokeTokenException();
}
return OAuth2Token(
accessToken: 'top_secret_refreshed',
);
},
shouldRefresh: (_) => Random().nextInt(2) == 0,
)..authenticationStatus.listen(print);
print(freshLink.token);
print(freshLink.authenticationStatus);
await freshLink
.setToken(OAuth2Token(tokenType: 'Bearer', accessToken: accessToken));
return freshLink;
}
Any solution, even without fresh_graphql, would be appreciated!

The way I initialize my ferry client is as follows.
Create a CustomAuthLink that inherits from AuthLink.
import 'package:gql_http_link/gql_http_link.dart';
class _CustomAuthLink extends AuthLink {
_CustomAuthLink() : super(
getToken: () {
// ...
// Call your api to refresh the token and return it
// ...
String token = await ... // api refresh call
return "Bearer $token"
}
);
}
Use this custom auth link to initialise your client.
...
final link = Link.from([freshLink, HttpLink('https://.../graphql/')]);
...
Client(
link: _CustomAuthLink().concat(link),
)
...
I am not sure if you still going to need freshLink anymore. You might wanna remove it and pass HttpLink(...) directly into the .concat(...) method.

Related

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!

Flutter web google_sign_in: How to retrieve refreshToken

google_sign_in does not return refreshToken. Is there a way to sign in with Google and receive a refresh token which can be sent to the API for further access to the user's data?
Refresh token could be also obtained with serverAuthCode which is always null at the moment. There are multiple issues created already describing this issue:
https://github.com/flutter/flutter/issues/45847
https://github.com/flutter/flutter/issues/57712
https://github.com/flutter/flutter/issues/15796
https://github.com/flutter/flutter/issues/62474
Is there any way to authenticate with Google Sign In and receive either refreshToken or serverAuthCode?
Google Sign In is based on oAuth2, one can create own implementation of the process.
Here's a code snippet of a google sign in service which can be used to retrieve refreshToken:
import 'dart:async';
import 'dart:html' as html;
import 'package:oauth2/oauth2.dart' as oauth2;
class GoogleSignInService {
final authorizationEndpoint =
Uri.parse('https://accounts.google.com/o/oauth2/v2/auth');
final tokenEndpoint = Uri.parse('https://oauth2.googleapis.com/token');
final String identifier;
final String secret;
final String baseUrl;
final List<String> scopes;
_SignInSession? _signInSession;
Uri get redirectUrl => Uri.parse('$baseUrl/callback.html');
GoogleSignInService({
required this.identifier,
required this.secret,
required this.baseUrl,
required this.scopes,
}) {
html.window.addEventListener('message', _eventListener);
}
void _eventListener(html.Event event) {
_signInSession?.completeWithCode((event as html.MessageEvent).data);
}
Future<GoogleSignInUser?> signIn() async {
if (_signInSession != null) {
return null;
}
final grant = oauth2.AuthorizationCodeGrant(
identifier,
authorizationEndpoint,
tokenEndpoint,
secret: secret,
);
var authorizationUrl = grant.getAuthorizationUrl(
redirectUrl,
scopes: scopes,
);
final url =
'${authorizationUrl.toString()}&access_type=offline&prompt=select_account+consent';
_signInSession = _SignInSession(url);
final code = await _signInSession!.codeCompleter.future;
if (code != null) {
final client = await grant.handleAuthorizationResponse({'code': code});
return GoogleSignInUser(
accessToken: client.credentials.accessToken,
refreshToken: client.credentials.refreshToken,
idToken: client.credentials.idToken!,
);
} else {
return null;
}
}
}
class GoogleSignInUser {
final String accessToken;
final String? refreshToken;
final String idToken;
const GoogleSignInUser({
required this.accessToken,
required this.refreshToken,
required this.idToken,
});
#override
String toString() {
return 'GoogleSignInUser{accessToken: $accessToken, refreshToken: $refreshToken, idToken: $idToken}';
}
}
class _SignInSession {
final codeCompleter = Completer<String?>();
late final html.WindowBase _window;
late final Timer _timer;
bool get isClosed => codeCompleter.isCompleted;
_SignInSession(String url) {
_window =
html.window.open(url, '_blank', 'location=yes,width=550,height=600');
_timer = Timer.periodic(const Duration(milliseconds: 500), (timer) {
if (_window.closed == true) {
if (!isClosed) {
codeCompleter.complete(null);
}
_timer.cancel();
}
});
}
void completeWithCode(String code) {
if (!isClosed) {
codeCompleter.complete(code);
}
}
}
Make sure to also create web/callback.html file:
<html>
<body>
</body>
<script>
function findGetParameter(parameterName) {
var result = null,
tmp = [];
location.search
.substr(1)
.split("&")
.forEach(function (item) {
tmp = item.split("=");
if (tmp[0] === parameterName) result = decodeURIComponent(tmp[1]);
});
return result;
}
let code = findGetParameter('code');
window.opener.postMessage(code, "http://localhost:5000");
window.close();
</script>
</html>
Change http://localhost:5000 to whatever domain you're using in production.
Exemplary usage:
final googleSignIn = GoogleSignInService(
identifier: 'CLIENT_ID',
secret: 'CLIENT_SECRET',
baseUrl: 'http://localhost:5000',
scopes: [
'email',
],
),
final user = await googleSignIn.signIn();

How do I hook up a cross platform Flutter app with Azure AD

I have a cross platform application (mobile, desktop and web) created in Flutter that I would like to set up to be authenticated with Azure AD.
I know that there are some packages that you can add for mobile and maybe even for web but I am unable to find a working solution for desktop.
I thought that I could open the browser on the device and use that to sign the user in, but it would need a URI to redirect to when the user is authenticated and for the application to be able to get the token that I can then use to make calls to my API. I can't see how that would work though, due to the application being hosted on the users device and not on a server with a set IP like with websites.
Any possible solutions or guidance would be greatly appreciated.
I ended up using a combination of this older tutorial for Facebook authentication along with Microsoft documentation on how to get a token for native apps to create a small authenticating service seen below.
I used the following pub packages:
url_launcher
flutter_dotenv
http
Auth Service:
import 'dart:async';
import 'dart:io';
import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:research_library_viewer/Models/Token.dart';
import 'package:url_launcher/url_launcher.dart';
import 'package:http/http.dart' as http;
class AuthenticationService {
String tenant = dotenv.env['MSAL_TENANT']!;
String clientId = dotenv.env['MSAL_CLIENT_ID']!;
String clientSecret = dotenv.env['MSAL_CLIENT_SECRET']!;
String redirectURI = dotenv.env['MSAL_LOGIN_REDIRECT_URI']!;
String scope = dotenv.env['MSAL_CLIENT_SCOPE']!;
String authority = dotenv.env['MSAL_AUTHORITY_URI']!;
Future<Stream<String>> _server() async {
final StreamController<String> onCode = StreamController();
HttpServer server =
await HttpServer.bind(InternetAddress.loopbackIPv4, 8080);
server.listen((HttpRequest request) async {
final String? code = request.uri.queryParameters["code"];
request.response
..statusCode = 200
..headers.set("Content-Type", ContentType.html.mimeType)
..write("<html><h1>You can now close this window</h1></html>");
await request.response.close();
await server.close(force: true);
if (code != null) {
onCode.add(code);
await onCode.close();
}
});
return onCode.stream;
}
String getAuthUrl() {
String authUrl =
"http://$authority/$tenant/oauth2/v2.0/authorize?client_id=$clientId&response_type=code&redirect_uri=$redirectURI&response_mode=query&scope=$scope";
return authUrl;
}
Map<String, dynamic> getTokenParameters(String token, bool refresh) {
Map<String, dynamic> tokenParameters = <String, dynamic>{};
tokenParameters["client_id"] = clientId;
tokenParameters["scope"] = scope;
tokenParameters["client_secret"] = clientSecret;
if (refresh) {
tokenParameters["refresh_token"] = token;
tokenParameters["grant_type"] = "refresh_token";
} else {
tokenParameters["code"] = token;
tokenParameters["redirect_uri"] = redirectURI;
tokenParameters["grant_type"] = "authorization_code";
}
return tokenParameters;
}
Future<Token> getToken() async {
String url = getAuthUrl();
Stream<String> onCode = await _server();
if (await canLaunch(url)) {
await launch(url);
} else {
throw "Could not launch $url";
}
final String code = await onCode.first;
final Map<String, dynamic> tokenParameters =
getTokenParameters(code, false);
final response = await http.post(
Uri.https(
'login.microsoftonline.com',
'$tenant/oauth2/v2.0/token',
),
headers: <String, String>{
'Content-Type': 'application/x-www-form-urlencoded'
},
body: tokenParameters);
if (response.statusCode == 200) {
return tokenFromJson(response.body);
} else {
throw Exception('Failed to acquire token');
}
}
Future<Token> refreshToken(String? refreshToken) async {
if (refreshToken == null) {
return getToken();
} else {
final Map<String, dynamic> tokenParameters = getTokenParameters(refreshToken, true);
final response = await http.post(
Uri.https(
'login.microsoftonline.com',
'$tenant/oauth2/v2.0/token',
),
headers: <String, String>{
'Content-Type': 'application/x-www-form-urlencoded'
},
body: tokenParameters);
if (response.statusCode == 200) {
return tokenFromJson(response.body);
} else {
throw Exception('Failed to acquire token');
}
}
}
}
Token:
import 'dart:convert';
Token tokenFromJson(String str) {
final jsonData = json.decode(str);
return Token.fromJson(jsonData);
}
class Token {
String accessToken;
String tokenType;
num? expiresIn;
String? refreshToken;
String? idToken;
String? scope;
Token({
required this.accessToken,
required this.tokenType,
this.expiresIn,
this.refreshToken,
this.idToken,
this.scope,
});
factory Token.fromJson(Map<String, dynamic> json) => Token(
accessToken: json["access_token"],
tokenType: json["token_type"],
expiresIn: json["expires_in"],
refreshToken: json["refresh_token"],
idToken: json["id_token"],
scope: json["scope"],
);
Map<String, dynamic> toJson() => {
"access_token": accessToken,
"token_type": tokenType,
"expires_in": expiresIn,
"refresh_token": refreshToken,
"id_token": idToken,
"scope": scope,
};
}
I think that this could still be improved a lot, but it is definitely something to start with if you are sitting with a similar challenge.
Found an MS document you can follow to add Azure Authentication in your Desktop application.
Refer this : Sign-in a user with the Microsoft Identity Platform in a WPF Desktop application and call an ASP.NET Core Web API
There is also another way for the same but with Azure AD B2C : Configure authentication in a sample WPF desktop app by using Azure AD B2C
The application registration and architecture are illustrated in the following diagrams:

Authorization Token of Pages

I have this error;
Could there be a problem with the token.It says Uri invalid token, but I login beforehand, why can't I read it? Headers I add token like this..But it isnt work how solve it ? What could I have done wrong?
And So map, ifwhere, where or other things dont work..
I'm new to this platform, I may have asked something very simple, sorry for that.
Thank you for all ....
import 'dart:async';
import 'package:bloc/bloc.dart';
import 'package:equatable/equatable.dart';
import 'package:flutter/material.dart';
import 'package:flutter_knowin_app/core/data/response.dart';
import 'package:flutter_knowin_app/core/util/constants.dart';
import 'package:flutter_knowin_app/features/promotions/domain/entities/check.dart';
import 'package:flutter_knowin_app/features/promotions/domain/entities/spin.dart';
import 'package:flutter_knowin_app/features/promotions/domain/repositories/spin_repository.dart';
import 'package:flutter_knowin_app/features/promotions/domain/usecases/check_regular.dart';
import 'package:flutter_knowin_app/features/promotions/domain/usecases/check_vip.dart';
import 'package:flutter_knowin_app/injection_container.dart';
part 'spin_event.dart';
part 'spin_state.dart';
enum SpinPageState {
regular,
vip,
}
class SpinBloc extends Bloc<SpinEvent, SpinState> {
final SpinRepository spinRepository = sl<SpinRepository>();
SpinPageState spinPageState;
bool stateVip;
bool spinState = false;
String winThing;
int winResult;
int gamePlayCountVip;
int gamePlayCountRegular;
checksRegular ChecksRegular;
checksVip ChecksVip;
SpinBloc({
#required checksRegular ChecksRegular,
#required checksVip ChecksVip,
})
: assert(checksRegular != null),
assert(checksVip != null),
ChecksRegular = ChecksRegular,
ChecksVip = ChecksVip,
super(SpinState.initial());
#override
Stream<SpinState> mapEventToState(SpinEvent event,) async* {
final SpinRepository spinRepository = sl<SpinRepository>();
SpinState.initial();
if (event is SpinLoadingEvent) {
Response updateResponse;
yield SpinLoadingState();
final checkRegularOrFailure = await ChecksRegular(
checksRegularParams());
final checkVipOrFailure = await ChecksVip(
checksVipParams());
checkRegularOrFailure.fold((failure) {
updateResponse = Response(
status: false,
message: UNEXPECTED_FAILURE_MESSAGE,
statusCode: "FAIL CHECK REGULAR",
);
}, (response) {
spinState = true;
spinPageState = SpinPageState.regular;
response.toList().where((e){
print("PEOPLE COUNT= ${ e.gamePlayed}");
});
});
checkVipOrFailure.fold((failure) {
updateResponse = Response(
status: false,
message: UNEXPECTED_FAILURE_MESSAGE,
statusCode: "FAIL CHECK VIP",
);
}, (response) {
spinState = true;
spinPageState = SpinPageState.vip;
});
yield SpinLoadingState();
}
if (event is SpinStartEvent) {
}
}
}
RemoteCode;
class PromotionRemoteDataSourceImpl implements PromotionRemoteDataSource {
String token;
final http.Client client;
PromotionRemoteDataSourceImpl({this.client});
Map<String, String> get defaultHeaders =>
{
'Accept': 'application/json',
'Authorization': 'Bearer $token',
"Content-Type": "application/json"
};
Map<String, String> get defaultQueryParams => {
"lang": Platform.localeName.split('_')[0],
};
#override
Future<List<Check>> checksRegular() async {
Uri uri = Uri.https(
API_URL,
'/promotions/check/regular',
defaultQueryParams,
);
print("URÄ°: "+ uri.toString());
final response = await client.get(uri, headers: defaultHeaders);
final body= json.decode(response.body);
I solved the error like this;
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
sl<PromotionRemoteDataSource>().token = userState.token;//!!!
});
}
I think the problem is here
response.toList().where((e){
print("PEOPLE COUNT= ${ e.gamePlayed}");
});
The reason why is as the error message stated. The response variable is null, and you're trying to convert null to a list and Dart didn't like that. Implement some way to deal with the scenario where response is null, e.g. have an if statement there, and that should hopefully solve your problem

How to cache the response of API calls in Flutter?

I am working in Flutter App to get the items from API. I want to cache the API response for 12 hours. Every 12 hours the response will be changed. Once the 12 hours completed then I need to fetch it from Internet. I used the below code to get it from internet.
Future<List<Playlist>> fetchPlaylistByChannelId({String channelId}) async {
Map<String, String> parameters = {
'part': 'snippet,contentDetails',
'channelId': channelId,
'maxResults': '10',
'key': API_KEY,
};
Uri uri = Uri.https(
_baseUrl,
'/youtube/v3/playlists',
parameters,
);
Map<String, String> headers = {
HttpHeaders.contentTypeHeader: 'application/json',
};
// Get Playlist details
var response = await http.get(uri, headers: headers);
if (response.statusCode == 200) {
var data = json.decode(response.body);
List<dynamic> playListJson = data['items'];
// Fetch all play list
List<Playlist> playLists = [];
playListJson.forEach(
(json) => playLists.add(
Playlist.fromMap(
json["id"],
json["snippet"],
json["contentDetails"],
),
),
);
return playLists;
} else {
throw json.decode(response.body)['error']['message'];
} }
Please help me out this.
Include flutter_cache_manager in pubspec.yaml.
Now define a cache manager
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
import 'package:http/http.dart' as http;
import 'dart:io';
import 'package:path_provider/path_provider.dart';
import 'package:path/path.dart' as path;
// Custom Implementation of CacheManager
// by extending the BaseCacheManager abstract class
class MyCacheManager extends BaseCacheManager {
static const key = "customCache";
static MyCacheManager _instance;
// singleton implementation
// for the custom cache manager
factory MyCacheManager() {
if (_instance == null) {
_instance = new MyCacheManager._();
}
return _instance;
}
// pass the default setting values to the base class
// link the custom handler to handle HTTP calls
// via the custom cache manager
MyCacheManager._()
: super(key,
maxAgeCacheObject: Duration(hours: 12),
maxNrOfCacheObjects: 200,
fileFetcher: _myHttpGetter);
#override
Future<String> getFilePath() async {
var directory = await getTemporaryDirectory();
return path.join(directory.path, key);
}
static Future<FileFetcherResponse> _myHttpGetter(String url,
{Map<String, String> headers}) async {
HttpFileFetcherResponse response;
// Do things with headers, the url or whatever.
try {
var res = await http.get(url, headers: headers);
// add a custom response header
// to regulate the caching time
// when the server doesn't provide cache-control
res.headers.addAll({'cache-control': 'private, max-age=120'});
response = HttpFileFetcherResponse(res);
} on SocketException {
print('No internet connection');
}
return response;
}
}
Now use
class HttpProvider {
Future<Response> getData(String url, Map<String, String> headers) async {
var file = await MyCacheManager().getSingleFile(url, headers: headers);
if (file != null && await file.exists()) {
var res = await file.readAsString();
return Response(res, 200);
}
return Response(null, 404);
}
}
Details at https://referbruv.com/blog/posts/caching-get-request-calls-using-flutter-cache-manager and https://proandroiddev.com/flutter-lazy-loading-data-from-network-with-caching-b7486de57f11
UPDATE: flutter_cache_manager 2.0.0
There is no longer a need to extend on BaseCacheManager, you can directly call the constructor. The BaseCacheManager is now only an interface. CacheManager is the implementation you can use directly.
check here
Another way of caching is by using hive a No-SQL database it is faster to retrieve documents and is easy to use. And when users come online just refresh the data in hive
For more details check:https://github.com/shashiben/Anime-details to know how to cache using hive