Persistency (return cached values if server is not available)

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],
        );
}