Hello, here I want to discuss about caching in flutter applications with parse server sdk
I am using dio_cache interceptor
DioCacheInterceptor(
options: CacheOptions(
store: cacheStore,
policy: CachePolicy.refreshForceCache,
hitCacheOnErrorExcept: [401, 403, 209, 404],
maxStale: const Duration(days: 999),
priority: CachePriority.high,
),
),
how should I handle user’s session token changes?
I’ve just copied ParseDioClient:
// ignore_for_file: unused_element
import 'package:dio/dio.dart' as dio;
import 'package:dio_cache_interceptor/dio_cache_interceptor.dart';
import 'package:dio_smart_retry/dio_smart_retry.dart';
import 'package:flutter/foundation.dart';
import 'package:parse_server_sdk_flutter/parse_server_sdk_flutter.dart';
import 'dio_adapter_io.dart';
class CustomParseDioClient extends ParseClient {
// securityContext is SecurityContext
CustomParseDioClient({bool sendSessionId = false, dynamic securityContext, required CacheStore? cacheStore}) {
_client = _CustomParseDioClient(
sendSessionId: sendSessionId,
securityContext: securityContext,
cacheStore: cacheStore,
);
}
late _CustomParseDioClient _client;
@override
Future<ParseNetworkResponse> get(
String path, {
ParseNetworkOptions? options,
ProgressCallback? onReceiveProgress,
}) async {
try {
final dio.Response<dynamic> dioResponse = await _client.get<dynamic>(
path,
options: _Options(headers: options?.headers),
);
return ParseNetworkResponse(
data: dioResponse.data!.toString(),
statusCode: dioResponse.statusCode! == 304 ? 200 : dioResponse.statusCode!,
);
} on dio.DioException catch (error) {
String? errorText;
if (error.response?.data is Map<String, dynamic>) {
errorText = error.response?.data['error'];
}
return ParseNetworkResponse(
data: errorText ?? error.response?.data ?? _fallbackErrorData,
statusCode: error.response?.statusCode ?? ParseError.otherCause,
);
}
}
@override
Future<ParseNetworkByteResponse> getBytes(
String path, {
ParseNetworkOptions? options,
ProgressCallback? onReceiveProgress,
dynamic cancelToken,
}) async {
try {
final dio.Response<List<int>> dioResponse = await _client.get<List<int>>(
path,
cancelToken: cancelToken,
onReceiveProgress: onReceiveProgress,
options: _Options(headers: options?.headers, responseType: dio.ResponseType.bytes),
);
return ParseNetworkByteResponse(
bytes: dioResponse.data,
statusCode: dioResponse.statusCode!,
);
} on dio.DioException catch (error) {
if (error.response != null) {
return ParseNetworkByteResponse(
data: error.response?.data ?? _fallbackErrorData,
statusCode: error.response?.statusCode ?? ParseError.otherCause,
);
} else {
return _getOtherCaseErrorForParseNetworkResponse(error.error.toString());
}
}
}
@override
Future<ParseNetworkResponse> put(String path, {String? data, ParseNetworkOptions? options}) async {
try {
final dio.Response<String> dioResponse = await _client.put<String>(
path,
data: data,
options: _Options(headers: options?.headers),
);
return ParseNetworkResponse(
data: dioResponse.data!,
statusCode: dioResponse.statusCode!,
);
} on dio.DioException catch (error) {
String? errorText;
if (error.response?.data is Map<String, dynamic>) {
errorText = error.response?.data['error'];
}
return ParseNetworkResponse(
data: errorText ?? error.response?.data ?? _fallbackErrorData,
statusCode: error.response?.statusCode ?? ParseError.otherCause,
);
}
}
@override
Future<ParseNetworkResponse> post(String path, {String? data, ParseNetworkOptions? options}) async {
try {
final dio.Response<String> dioResponse = await _client.post<String>(
path,
data: data,
options: _Options(headers: options?.headers),
);
return ParseNetworkResponse(
data: dioResponse.data!,
statusCode: dioResponse.statusCode!,
);
} on dio.DioException catch (error) {
String? errorText;
if (error.response?.data is Map<String, dynamic>) {
errorText = error.response?.data['error'];
}
return ParseNetworkResponse(
data: errorText ?? error.response?.data ?? _fallbackErrorData,
statusCode: error.response?.statusCode ?? ParseError.otherCause,
);
}
}
@override
Future<ParseNetworkResponse> postBytes(String path,
{Stream<List<int>>? data,
ParseNetworkOptions? options,
ProgressCallback? onSendProgress,
dynamic cancelToken}) async {
try {
final dio.Response<String> dioResponse = await _client.post<String>(
path,
data: data,
cancelToken: cancelToken,
options: _Options(headers: options?.headers),
onSendProgress: onSendProgress,
);
return ParseNetworkResponse(
data: dioResponse.data!,
statusCode: dioResponse.statusCode!,
);
} on dio.DioException catch (error) {
if (error.response != null) {
String? errorText;
if (error.response?.data is Map<String, dynamic>) {
errorText = error.response?.data['error'];
}
return ParseNetworkResponse(
data: errorText ?? error.response?.data ?? _fallbackErrorData,
statusCode: error.response?.statusCode ?? ParseError.otherCause,
);
} else {
return _getOtherCaseErrorForParseNetworkResponse(error.error.toString());
}
}
}
_getOtherCaseErrorForParseNetworkResponse(String error) {
return ParseNetworkResponse(
data: "{\"code\":${ParseError.otherCause},\"error\":\"$error\"}", statusCode: ParseError.otherCause);
}
@override
Future<ParseNetworkResponse> delete(String path, {ParseNetworkOptions? options}) async {
try {
final dio.Response<String> dioResponse = await _client.delete<String>(
path,
options: _Options(headers: options?.headers),
);
return ParseNetworkResponse(
data: dioResponse.data!,
statusCode: dioResponse.statusCode!,
);
} on dio.DioException catch (error) {
String? errorText;
if (error.response?.data is Map<String, dynamic>) {
errorText = error.response?.data['error'];
}
return ParseNetworkResponse(
data: errorText ?? error.response?.data ?? _fallbackErrorData,
statusCode: error.response?.statusCode ?? ParseError.otherCause,
);
}
}
String get _fallbackErrorData => '{"$keyError":"NetworkError"}';
}
/// Creates a custom version of HTTP Client that has Parse Data Preset
class _CustomParseDioClient with dio.DioMixin implements dio.Dio {
_CustomParseDioClient({
bool sendSessionId = false,
dynamic securityContext,
required CacheStore? cacheStore,
}) : _sendSessionId = sendSessionId {
options = dio.BaseOptions();
httpClientAdapter = createHttpClientAdapter(securityContext);
interceptors.addAll([
RetryInterceptor(
dio: this,
retries: 3,
retryEvaluator: (error, attempt) async {
return error.type != dio.DioExceptionType.connectionError &&
![
401,
403,
404,
].contains(error.response?.statusCode);
},
retryDelays: [
const Duration(seconds: 1),
const Duration(seconds: 3),
],
),
DioCacheInterceptor(
options: CacheOptions(
store: cacheStore,
policy: CachePolicy.refreshForceCache,
hitCacheOnErrorExcept: [401, 403, 209, 404],
maxStale: const Duration(days: 999),
priority: CachePriority.high,
),
),
]);
}
final bool _sendSessionId;
final String _userAgent = '$keyLibraryName $keySdkVersion';
ParseCoreData parseCoreData = ParseCoreData();
Map<String, String>? additionalHeaders;
/// Overrides the call method for HTTP Client and adds custom headers
@override
Future<dio.Response<T>> request<T>(
String path, {
dynamic data,
Map<String, dynamic>? queryParameters,
dio.CancelToken? cancelToken,
dio.Options? options,
ProgressCallback? onSendProgress,
ProgressCallback? onReceiveProgress,
}) {
options ??= dio.Options();
options.headers ??= <String, dynamic>{};
if (!identical(0, 0.0)) {
options.headers![keyHeaderUserAgent] = _userAgent;
}
options.headers![keyHeaderApplicationId] = parseCoreData.applicationId;
if (_sendSessionId && parseCoreData.sessionId != null && options.headers![keyHeaderSessionToken] == null) {
options.headers![keyHeaderSessionToken] = parseCoreData.sessionId;
}
if (parseCoreData.clientKey != null) {
options.headers![keyHeaderClientKey] = parseCoreData.clientKey;
}
if (parseCoreData.masterKey != null) {
options.headers![keyHeaderMasterKey] = parseCoreData.masterKey;
}
/// If developer wants to add custom headers, extend this class and add headers needed.
if (additionalHeaders != null && additionalHeaders!.isNotEmpty) {
additionalHeaders!.forEach((String key, String value) => options!.headers![key] = value);
}
if (parseCoreData.debug) {
_logCUrl(options, data, path);
}
checkForSubmitEventually();
return super.request(
path,
data: data,
queryParameters: queryParameters,
cancelToken: cancelToken,
options: options,
onSendProgress: onSendProgress,
onReceiveProgress: onReceiveProgress,
);
}
void _logCUrl(dio.Options options, dynamic data, String url) {
String curlCmd = 'curl';
curlCmd += ' -X ${options.method!}';
bool compressed = false;
options.headers!.forEach((String name, dynamic value) {
if (name.toLowerCase() == 'accept-encoding' && value?.toString().toLowerCase() == 'gzip') {
compressed = true;
}
curlCmd += ' -H \'$name: $value\'';
});
curlCmd += (compressed ? ' --compressed ' : ' ') + url;
curlCmd += '\n\n ${Uri.decodeFull(url)}';
if (kDebugMode) {
print('╭-- Parse Request');
print(curlCmd);
print('╰--');
}
}
}
class _Options extends dio.Options {
_Options({
super.method,
super.sendTimeout = const Duration(seconds: 120),
super.receiveTimeout = const Duration(seconds: 10),
super.extra,
super.headers,
super.responseType,
String? contentType,
super.validateStatus,
super.receiveDataWhenStatusError,
super.followRedirects,
super.maxRedirects,
super.requestEncoder,
super.responseDecoder,
}) : super(
contentType: contentType ?? (headers ?? <String, dynamic>{})[dio.Headers.contentTypeHeader],
);
}