likes
comments
collection
share

【Flutter】自定义文件缓存管理与网络请求的缓存策略

作者站长头像
站长
· 阅读数 5

文件缓存管理类与网络请求的缓存策略

前言

一个应用的缓存我们一般用到三种方式,SharedPreferences,File文件,SQLite数据库。

三种方式各有各自的优缺点,SharedPreferences 适用于存储简单的键值对数据,File 文件适用于自由组织的任意类型数据,而 SQLite 数据库适用于存储大量结构化数据并进行复杂查询操作。

这种问题没有人比 ChatGPT 回答的更标准了:

【Flutter】自定义文件缓存管理与网络请求的缓存策略

一般来说我们用的比较多的就是 SP 与 File,毕竟没有那种高级查询需求非必要不需要盲目上数据库。

我们在学习 Android 开始就知道 SP 不要存大数据,而实际开发中中 iOS 同事与前端同事都是啥都往 SP 里面存的。网络请求数据,用户信息数据等等。这...也不是不行,只是会导致启动越来越慢了。

哎!怎会如此?这...偏题了啊,这些比较基础的内容就不占用篇幅了,咱们可以自行搜索查询是吧。

没时间解释了,快点上车吧。怎么用文件缓存?

我相信大家都已经导入了 path_provider 插件了。为了兼容各平台,我们的缓存文件最好使用的是 getTemporaryDirectory 目录。

接下来我们就就自己创建一个缓存文件管理类吧!

一、文件缓存管理

首先我们先用一个工具类管理一下 'path_provider' 插件的文件夹创建与管理。

class DirectoryUtil {

  ...

    Future _init() async {
      await initTempDir();
    }


  static Future<Directory?> initTempDir() async {
    if (_tempDir == null) {
      _tempDir = await getTemporaryDirectory();
    }
    return _tempDir;
  }

  static Future<Directory?> createTempDir({String? category}) async {
    await initTempDir();
    String? path = getTempPath(category: category);
    return createDir(path);
  }

   static String? getTempPath({
    String? category,
    String? fileName,
    String? format,
  }) {
    return getPath(_tempDir,
        category: category, fileName: fileName, format: format);
  }

  static String? getPath(
    Directory? dir, {
    String? category,
    String? fileName,
    String? format,
  }) {
    if (dir == null) return null;
    StringBuffer sb = StringBuffer("${dir.path}");
    if (category != null) sb.write("/$category");
    if (fileName != null) sb.write("/$fileName");
    if (format != null) sb.write(".$format");
    return sb.toString();
  }
  ...
}

这里只贴出了关键代码,主要用于初始化缓存文件的管理类,用于创建Cache内部的文件夹与文件。

接下来就是真正的缓存文件管理类:

1.1 设置过期时间,储存数据格式的问题

首先我们需要确定我们存储进去的是什么格式,我们直接拿到了Http的响应之后直接把Json对象以字符串的形式存入吗?

可以是可以,如果我设置最大缓存容量,需要删除之前的缓存,我怎么判断?需要传入创建文件的时间戳吧!

如果我想指定缓存文件的过期时间如何判断?需要写入过期时间戳吧。

所以我们写入的数据最好为一个 Json 格式。

//可以用 fileCache 直接使用更方便
var fileCache = FileCacheManager();

class FileCacheManager {
  static const String _fileCategory = "app_file_cache";
  
  //单例
  FileCacheManager._internal();
  static final FileCacheManager _singleton = FileCacheManager._internal();
  factory FileCacheManager() => _singleton;


  // 初始化 - 获取到缓存路径,创建指定缓存文件夹
  Future _init() async {
    cachePath = DirectoryUtil.getTempPath(category: _fileCategory,fileName: 'allcache');
    if (cachePath != null) {
      await DirectoryUtil.createTempDir(category: _fileCategory);
    } else {
      throw Exception('DirectoryUtil 无法获取到Cache文件夹,可能 DirectoryUtil 没有初始化,请检查!');
    }
  }

  Future<void> putJsonByKey(String key, Map<String, dynamic> jsonData, {Duration? expiration}) async {
 
      if (cachePath == null) {
        await _init();
      }else {
        Directory cacheDir = Directory(cachePath??'');
        if (!await cacheDir.exists()) {
          await DirectoryUtil.createTempDir(category: _fileCategory);
        }
      }

      final file = File('$cachePath');
      Map<String, dynamic> existingData = {};

      //获取到已经存在的 Json
      if (await file.exists()) {
        final jsonString = await file.readAsString();
        existingData = jsonDecode(jsonString) as Map<String, dynamic>;
      }

      //存入现有的 key - value 缓存
      existingData[key] = {
        'data': jsonData,
        'timestamp': DateTime.now().millisecondsSinceEpoch,
        'expiration': expiration?.inMilliseconds,
      };

      //转化为新的Json文本
      final newJsonString = jsonEncode(existingData);

      //覆盖写入到文件
       await file.writeAsString(jsonString);
 
  }

}

我们已经可以简单的写入到缓存文件了,可是这样真的好吗?

1.2 一个缓存文件还是多个缓存文件的问题

在上面的示例中,我们写入的文件缓存是在同一个文件中,也就是 data/data/pageageName/cache/app_file_cache/allcache 文件中。

如果我存入的文件多了,每次都要复写整个文件,效率慢!

如果我存入的 Json 多了,还需要取出整个文件再按 key 查找我们的数据 Json 对象,再判断过期时间,再对 Json 对象的 data 真正数据进行操作!效率慢!

如果我想指定删除 key 呢?麻烦!也需要覆写整个缓存文件。

如果我想限制整体缓存文件大小呢? 一样需要删除指定Key,如果遍历执行还需要多粗覆写整个缓存文件。

无法接收,所以目前文件缓存的更主流做法还是推荐使用单独缓存单个文件的做法。

读取快速,无需多次查询,方便删除key,方便做最大缓存限制管理。

怎么改呢?只需要初始化的时候传入 Cacha 下面的 Category 文件夹即可,具体的文件创建与写入由具体方法来操作。

//可以用 fileCache 直接使用更方便
var fileCache = FileCacheManager();

class FileCacheManager {
  static const String _fileCategory = "app_file_cache";
  
  //单例
  FileCacheManager._internal();
  static final FileCacheManager _singleton = FileCacheManager._internal();
  factory FileCacheManager() => _singleton;
  static final Lock _lock = Lock();

  String? cachePath;

  // 初始化 - 获取到缓存路径,创建指定缓存文件夹
  Future _init() async {
    cachePath = DirectoryUtil.getTempPath(category: _fileCategory);
    if (cachePath != null) {
      await DirectoryUtil.createTempDir(category: _fileCategory);
    } else {
      throw Exception('DirectoryUtil 无法获取到Cache文件夹,可能 DirectoryUtil 没有初始化,请检查!');
    }
  }

 /// 添加Json数据到本地文件缓存
  Future<void> putJsonByKey(String key, Map<String, dynamic> jsonData, {Duration? expiration}) async {
    // 加锁
    await _lock.synchronized(() async {
      if (cachePath == null) {
        await _init();
      }else {
        Directory cacheDir = Directory(cachePath??'');
        if (!await cacheDir.exists()) {
          await DirectoryUtil.createTempDir(category: _fileCategory);
        }
      }

      final file = File('$cachePath/$key');
      Map<String, dynamic> existingData = {};

      //获取到已经存在的 Json
      if (await file.exists()) {
        final jsonString = await file.readAsString();
        existingData = jsonDecode(jsonString) as Map<String, dynamic>;
      }

      //存入现有的 key - value 缓存
      existingData[key] = {
        'data': jsonData,
        'timestamp': DateTime.now().millisecondsSinceEpoch,
        'expiration': expiration?.inMilliseconds,
      };

      //转化为新的Json文本
      final newJsonString = jsonEncode(existingData);

      await file.writeAsString(jsonString);
    });
  }

  /// 从本都缓存文件读取Json数据
  Future<Map<String, dynamic>?> getJsonByKey(String key) async {
    if (cachePath == null) {
      await _init();
    }else {
      Directory cacheDir = Directory(cachePath??'');
      if (!await cacheDir.exists()) {
        await DirectoryUtil.createTempDir(category: _fileCategory);
      }
    }

    final jsonData = await _readAllJsonFromFile(key);
    if (jsonData == null) {
      return null;
    }

    //取出对应 key 的Json文本
    final jsonEntry = jsonData[key] as Map<String, dynamic>?;
    if (jsonEntry == null || _isExpired(jsonEntry)) {
      return null;
    }

    //返回去除过期时间之后的真正数据
    return jsonEntry['data'] as Map<String, dynamic>?;
  }

}

注意这里加锁了,为了避免多线程操作同一个key导致缓存混乱的情况。存入 Json与 取出 Json 的操作 就完成了。

既然 Josn 对象都能存取,那么基本数据对象也是一样的操作。

  /// 添加基本数据类型的数据到本地文件缓存
  Future<void> putValueByKey<T>(String key, T value, {Duration? expiration}) async {
    // 加锁
    await _lock.synchronized(() async {
      if (cachePath == null) {
        await _init();
      }else {
        Directory cacheDir = Directory(cachePath??'');
        if (!await cacheDir.exists()) {
          await DirectoryUtil.createTempDir(category: _fileCategory);
        }
      }

      final file = File('$cachePath/$key');
      Map<String, dynamic> existingData = {};

      //获取到已经存在的 Json
      if (await file.exists()) {
        final jsonString = await file.readAsString();
        existingData = jsonDecode(jsonString) as Map<String, dynamic>;
      }

      //存入现有的 key - value 缓存
      existingData[key] = {
        'data': value,
        'timestamp': DateTime.now().millisecondsSinceEpoch,
        'expiration': expiration?.inMilliseconds,
      };

      //转化为新的Json文本写入到文件
      final newJsonString = jsonEncode(existingData);
      await file.writeAsString(jsonString);
    });
  }

  /// 从本都缓存文件读取基本数据类型数据
  Future<T?> getValueByKey<T>(String key) async {
    if (cachePath == null) {
      await _init();
    }else {
      Directory cacheDir = Directory(cachePath??'');
      if (!await cacheDir.exists()) {
        await DirectoryUtil.createTempDir(category: _fileCategory);
      }
    }

    final jsonData = await _readAllJsonFromFile(key);
    if (jsonData == null) {
      return null;
    }

    //取出对应 key 的Json文本
    final jsonEntry = jsonData[key] as Map<String, dynamic>?;
    if (jsonEntry == null || _isExpired(jsonEntry)) {
      return null;
    }

    //返回去除过期时间之后的真正数据
    return jsonEntry['data'] as T?;
  }

  // 是否过期了
  bool _isExpired(Map<String, dynamic> jsonEntry) {
    final timestamp = jsonEntry['timestamp'] as int?;
    final expiration = jsonEntry['expiration'] as int?;
    if (timestamp != null && expiration != null) {
      final currentTime = DateTime.now().millisecondsSinceEpoch;
      return currentTime - timestamp > expiration;
    }
    return false;
  }

这里加入了是否过期的判断。

1.3 限制最大储存空间的问题

为什么要加上最大储存空间的限制,主要是为了对一些过期的缓存,长期不用的缓存做出清除。为了节约用户设备空间。也是为了自己的人生财产安全。

这里说一下之前听到的故事,开发者在车载App中写入文件缓存,没有限制最大缓存控件,导致无限写入文件到内存空间满了,车机死机无法重启,车辆召回导致巨大的经济损失,开发者需要进行经济赔赏。

虽然说当然咱们开发手机 App 应用的,设备不贵,也不是赔不起。但是咱们就是说没必要是不是,我个人觉得的话不管是文件缓存还是数据库缓存都需要限制总大小最好了,否则真出现内存空间满了出现一些不可预知的状况,得不偿失。

再说了如果你缓存没限制导致应用占用空间太大,用户也不傻,在设置中都是可以看到的,用户只会觉得你的应用辣鸡...

好了言归正传,如何限制最大存储空间呢?

如果是单文件缓存方案,那真是麻烦,但是我们使用了多文件缓存方案之后,我们只需要遍历我们自己缓存文件夹下面的文件即可,优先删除过期的文件,再删除存储时间最早的文件,直到能放入当前文件。

在每次想要存入文件的时候,我们需要调用验证方法,其中我们可以使用 while 循环来实现:

  // 检查是否超过最大限制,并写入文件
  Future<void> checkAndWriteFile(File file, String jsonString) async {
    Directory cacheDir = Directory(cachePath!);
    List<File> cacheFiles = cacheDir.listSync().whereType<File>().toList();

    if (cacheFiles.isNotEmpty) {
      // 计算缓存文件夹的总大小
      int totalSizeInBytes = 0;
      for (File file in cacheFiles) {
        totalSizeInBytes += await file.length();
      }

      //如果总大小超过限制,依次删除文件直到满足条件
      while (maxSizeInBytes > 0 && totalSizeInBytes > maxSizeInBytes) {
        File? fileToDelete;
        int oldestTimestamp = 0;

        for (File file in cacheFiles) {
          final key = path.basename(file.path);

          //取出全部的 Json 文本与对象
          final jsonString = await file.readAsString();
          final jsonData = jsonDecode(jsonString) as Map<String, dynamic>?;

          if (jsonData == null) {
            continue;
          }

          //取出对应 key 的 Json 对象
          final jsonEntry = jsonData[key] as Map<String, dynamic>?;
          if (jsonEntry == null || _isExpired(jsonEntry)) {
            fileToDelete = file;
            break;
          } else {
            final timestamp = jsonData['timestamp'] as int?;
            if (timestamp != null) {
              if (timestamp < oldestTimestamp) {
                fileToDelete = file;
                oldestTimestamp = timestamp;
              }
            }
          }
        }

        //遍历文件结束之后需要删除处理的逻辑
        if (fileToDelete != null) {
          Log.d('需要删除的文件:$fileToDelete');
          totalSizeInBytes -= await fileToDelete.length();
          await fileToDelete.delete();
          // 更新缓存文件列表
          cacheFiles.remove(fileToDelete);
        } else {
          break;
        }
      }

      //最后写入文件
      await file.writeAsString(jsonString);
    } else {
      // 如果是空文件夹,直接写即可
      await file.writeAsString(jsonString);
    }

     // 读取文件的全部Json数据
  Future<Map<String, dynamic>?> _readAllJsonFromFile(String key) async {
    String path = '$cachePath/$key';
    final file = File(path);
    if (await file.exists()) {
      final jsonString = await file.readAsString();
      final jsonData = jsonDecode(jsonString) as Map<String, dynamic>;
      return jsonData;
    }
    return null;
  }

  /// 移除指定的 Key 对应的缓存文件
  Future<void> removeByKey(String key) async {
    String path = '$cachePath/$key';
    final file = File(path);
    if (await file.exists()) {
      await file.delete();
    }
  }

  /// 移除全部的 Key 对应的缓存文件
  Future<void> removeAllKey() async {
    Directory cacheDir = Directory(cachePath!);
    if (await cacheDir.exists()) {
      List<File> cacheFiles = cacheDir.listSync().whereType<File>().toList();
      if (cacheFiles.isNotEmpty) {
        for (File file in cacheFiles) {
          if (await file.exists()) {
            await file.delete();
          }
        }
      }
    }
  }

}

关键的代码基本上都已经贴出。

当然了我这是最初级的实现,还有很大的优化空间,这个东西后期看时间再说吧,反正先用起来再说。

如果想要的话,我们现在就可以把一些对象存入缓存啦,把之前存入 SP 中的地址数据,用户详情数据之类的都放到文件缓存中管理了。

效果:

【Flutter】自定义文件缓存管理与网络请求的缓存策略

那么除此之外,还有文件缓存最大的应用场景 - 网络请求的缓存又如何实现呢?

二、网络请求的缓存策略

由于各种原因,我们并没有用 dio ,直接使用的 GetX 框架自带的 GetConnect 来进行网络请求,本质上还是 Dart 的 http 的封装。

这里就以此为思路说一下,如果想应用到 dio 上也是差不多的。稍微改动即可。

2.1 使用拦截器能实现请求拦截吗?

我们都知道 dio 有网络请求拦截器,我们的 GetConnect 其实也是有的,我们看名字就能看出:

    httpClient.baseUrl = ApiConstants.baseUrl;
    httpClient.timeout = const Duration(seconds: 30);

    // 统一添加身份验证请求头
    httpClient.addRequestModifier(authInterceptor);

    // 打印Log(生产模式去除)
    if (!AppConstant.inProduction) {
      httpClient.addRequestModifier(logReqInterceptor);
    }

    //统一对网络请求结果的处理
    httpClient.addResponseModifier(responseInterceptor);

addRequestModifier 只是添加了修改器,可以在网络请求发起前和响应做一些修改。

比如我们在拦截器中进行一些判断,返回自己的响应对象,但是网络请求并不能停止。

FutureOr<dynamic> responseInterceptor(Request request, Response response) async {

  Log.d('网络请求参数 : RequestUrl: ${request.url} , RequestMethod: ${request.method} , RequestHeaders:${request.headers}');

  if (request.headers['cache_control'] != null &&
      request.method == 'GET'
  ) {
   final cacheControl = request.headers['cache_control'];
    Log.d('需要缓存数据哦 cacheControl ==== > $cacheControl');
  }
}

也就是说,只要发起了请求,就必然会请求网络,虽然结果使我们的缓存结果,但是还是发起了请求,浪费了用户流量,关键是还是会等待网络请求的时间,此时就算有缓存,如果用户的设备断网了,还是无法获取到缓存。

所以我们需要在源头,发起请求的地方进行封装才行,下面看看如何对 GetConnect 进行修改。

我们在生成请求的方法中:

 Future<Response> _generateRequest(
    HttpMethod? method,
    Map<String, dynamic>? params,
    Map<String, String>? paths, //文件
    Map<String, Uint8List>? pathStreams, //文件流
    String url,
    Map<String, String>? headers,
  ) async {


    if (method != null && method == HttpMethod.POST) {
      ...
     //以 Post-FromData 的方式上传
      req = post(url, form, headers: headers);
    } else {
      //默认以 Get-Params 的方式上传
      req = return get(url, headers: headers, query: params);
    }
  }

我们只需要在 Get 请求进行缓存,比如我们可以根据是否缓存的变量,来决定返回的 Future 对象,是我们自己 new 的对象还是由 get 请求发起的。

示例:

      //有缓存用缓存,没缓存用网络请求并存入缓存
      final key = _generateKeyByUrlParams(url, params);
      final json = await FileCacheManager().getJsonByKey(key);
      if (json != null) {
        final completer = Completer<Response>();
        completer.complete(Response(
          statusCode: 200,
          body: json,
          statusText: '获取缓存成功',
        ));
        return completer.future;
      } else {
        
        //再去发起请求并存入文件缓存
        return get(url, headers: headers, query: params);
      }

_generateKeyByUrlParams 是根据 get 请求的 url 生成的 md5 加密之后的key:

  // 生成加密的Key
  String _generateKeyByUrlParams(String? url, Map<String, dynamic>? query) {
    if (httpClient.baseUrl != null) {
      url = httpClient.baseUrl! + url!;
    }

    final uri = Uri.parse(url!);
    if (query != null) {
      return EncryptUtil.encodeMd5(uri.replace(queryParameters: query).toString());
    }

    return EncryptUtil.encodeMd5(uri.toString());
  }

这样就可以达到效果,只要有缓存,不需要请求网络,断网也能请求到数据,如果没有缓存才真正的发起网络请求。

2.2 不同的策略如何处理?

如果有些 Get 请求我只想使用缓存,有些 Get 请求我只想用网络数据,但是程序可以帮我缓存起来。而最多使用的场景是有缓存用缓存,没缓存去网络请求并缓存起来。

这么多缓存情况,我应该如何判断并分别实现呢?

我们定义一个枚举,定义一些常用的缓存策略:

enum CacheControl {
  noCache, //不使用缓存
  onlyCache, //只用缓存
  cacheFirstOrNetworkPut, //有缓存先用缓存,没有缓存进行网络请求再存入缓存
  onlyNetworkPutCache, //只用网络请求,但是会存入缓存
}

在网络请求的入口传入参数控制:

  /*
   * 网络请求异步的结果 Result 的封装为自己的 HttpResult,以异步 Future 的方式返回
   * 最终数据仓库只需要处理自定义的 HttpResult 即可,在数据仓库中进行数据的转换与生成
   */
  Future<HttpResult> requestNetResult(
    String url, {
    HttpMethod? method, //指明Get还是Post请求
    Map<String, String>? headers, //请求头
    Map<String, dynamic>? params, //请求参数,Get的Params,Post的Form
    Map<String, String>? paths, //文件Flie
    Map<String, Uint8List>? pathStreams, //文件流
    CacheControl? cacheControl, // Get请求是否需要缓存
    Duration? cacheExpiration, //缓存是否需要过期时间,过期时间为多长时间
  }) async {
     
    Response response = await _generateRequest(method, params, paths, pathStreams, url, headers, cacheControl, cacheExpiration);

    ...
  }

 Future<Response> _generateRequest(
    HttpMethod? method,
    Map<String, dynamic>? params,
    Map<String, String>? paths, //文件
    Map<String, Uint8List>? pathStreams, //文件流
    String url,
    Map<String, String>? headers,
  ) async {


    if (method != null && method == HttpMethod.POST) {
      ...
     //以 Post-FromData 的方式上传
      req = post(url, form, headers: headers);
    } else {
      //默认以 Get-Params 的方式上传,处理 Cache 的自定义headers
      req = _handleGetCache(url, headers, params, cacheControl, cacheExpiration);
    }
  }

具体的策略判断:


Future<Response> _handleGetCache(
    String url,
    Map<String, String>? headers,
    Map<String, dynamic>? params,
    CacheControl? cacheControl,
    Duration? cacheExpiration,
  ) async {
    if (cacheControl?.name == CacheControl.onlyCache.name) {
      //直接返回缓存
      final key = _generateKeyByUrlParams(url, params);
      final json = await FileCacheManager().getJsonByKey(key);
      if (json != null) {
        final completer = Completer<Response>();
        completer.complete(Response(
          statusCode: 200,
          body: json,
          statusText: '获取缓存成功',
        ));
        return completer.future;
      } else {
        Log.e('没有缓存');
        final completer = Completer<Response>();
        completer.complete(Response(
          statusCode: 499,
          body: json,
          statusText: '获取网络缓存数据失败',
        ));
        return completer.future;
      }
    } else if (cacheControl?.name == CacheControl.cacheFirstOrNetworkPut.name) {

      //有缓存用缓存,没缓存用网络请求并存入缓存
      final key = _generateKeyByUrlParams(url, params);
      final json = await FileCacheManager().getJsonByKey(key);
      if (json != null) {
        final completer = Completer<Response>();
        completer.complete(Response(
          statusCode: 200,
          body: json,
          statusText: '获取缓存成功',
        ));
        return completer.future;
      } else {

        //再去发起请求并存入文件缓存
        return get(url, headers: headers, query: params);
      }
    } else if (cacheControl?.name == CacheControl.onlyNetworkPutCache.name) {
      final key = _generateKeyByUrlParams(url, params);

      //再去发起请求并存入文件缓存
      return get(url, headers: headers, query: params);
    } else {
      //默认网络请求
      return get(url, headers: headers, query: params);
    }
  }

注释很详细,这样就能根据不同的策略返回不同的响应对象了,但是网络请求之后最后如何存储呢?

2.3 如何标记网络请求需要缓存?

其实在 addResponseModifier 中虽然我们不能阻止网络请求,但是我们可以修改请求头与响应头,我们可以设置自定义的 header,最终处理响应数据的时候我们可以根据是否需要缓存来进行数据的缓存。

在 cacheFirstOrNetworkPut 和 onlyNetworkPutCache 这两种策略中我们需要缓存网络响应,那么我们就在这两种策略中加入自定义的请求头。

      //处理数据缓存需要的请求头
      if (headers == null || headers.isEmpty) {
        headers = <String, String>{};
      }
      headers['cache_control'] = cacheControl!.name;
      headers['cache_key'] = key;
      if (cacheExpiration != null) {
        headers['cache_expiration'] = cacheExpiration.inMilliseconds.toString();
      }

并且在 addResponseModifier 的拦截修改器中,进行 Response 的响应头的写入:

FutureOr<dynamic> responseInterceptor(Request request, Response response) async {

  bool isDialogShowing = false; // 控制弹窗是否正在显示
  if (response.statusCode == 401) {
    // 避免重复展示弹窗
    if (!isDialogShowing) {
      //弹框就清除了数据
      UserService.to.handleLogoutParams();

      //拦截 token 过期,弹出弹窗提示用户重新登录
      SmartDialog.show(
        usePenetrate: false,
        debounce: true,
        clickMaskDismiss: false,
        onDismiss: () {
          isDialogShowing = false;
        },
        builder: (context) => AppDefaultDialog(
          "登录凭证已过期,请重新登录".tr,
          confirmAction: () {
            Get.offAllNamed(RouterPath.AUTH_LOGIN);
          },
        ),
      );
      //设置已经展示
      isDialogShowing = true;
    }
  } else if (response.statusCode == 200){

    //成功的时候设置缓存数据放入 headers 中
    if (request.headers['cache_control'] != null) {

      final cacheExpiration = request.headers['cache_expiration'];

      response.headers?['cache_control'] = request.headers['cache_control']!;
      response.headers?['cache_key'] = request.headers['cache_key']!;
      if (!Utils.isEmpty(cacheExpiration)) {
        response.headers?['cache_expiration'] = cacheExpiration!;
      }

    }
  }

  return response;
}

最终拿到响应 response 的时候取出自定义的响应头中的参数判断是否需要缓存:


  /*
   * 网络请求异步的结果 Result 的封装为自己的 HttpResult,以异步 Future 的方式返回
   * 最终数据仓库只需要处理自定义的 HttpResult 即可,在数据仓库中进行数据的转换与生成
   */
  Future<HttpResult> requestNetResult(
    String url, {
    HttpMethod? method, //指明Get还是Post请求
    Map<String, String>? headers, //请求头
    Map<String, dynamic>? params, //请求参数,Get的Params,Post的Form
    Map<String, String>? paths, //文件Flie
    Map<String, Uint8List>? pathStreams, //文件流
    CacheControl? cacheControl, // Get请求是否需要缓存
    Duration? cacheExpiration, //缓存是否需要过期时间,过期时间为多长时间
  }) async {
     
    Response response = await _generateRequest(method, params, paths, pathStreams, url, headers, cacheControl, cacheExpiration);

    ...

      //网络请求完成之后获取正常的Json-Map
        Map<String, dynamic> jsonMap = response.body;

        //先处理缓存逻辑
        final cacheControl = response.headers?['cache_control'];
        if (cacheControl != null) {
          final cacheKey = response.headers?['cache_key'];
          final cacheExpiration = response.headers?['cache_expiration'];

          Log.d('response cacheControl ==== > $cacheControl cacheKey ==== > $cacheKey cacheExpiration ==== > '
              '$cacheExpiration');

          Duration? duration;
          if (cacheExpiration != null) {
            duration = Duration(milliseconds: int.parse(cacheExpiration));
          }

          fileCache.putJsonByKey(
            cacheKey ?? 'unknow',
            jsonMap,
            expiration: duration,
          );
        }

    ... 
  }

那么最终使用的时候我们不需要改动任何上层的请求代码,只需要在想要缓存的接口中添加一个可选参数即可:

【Flutter】自定义文件缓存管理与网络请求的缓存策略

最终的效果Log:

【Flutter】自定义文件缓存管理与网络请求的缓存策略

【Flutter】自定义文件缓存管理与网络请求的缓存策略

【Flutter】自定义文件缓存管理与网络请求的缓存策略

【Flutter】自定义文件缓存管理与网络请求的缓存策略

总结

注意:本文基于纯 Flutter App 项目实现,还是那句话如果是 Flutter Module + Native 的方案,那么实现的方式有很多种方案,毕竟 Native 各平台都有自己非常完备的解决方案。

目前用起来还算可以,当然本身框架有优化的地方

比如 totalSize 可以使用全局变量加载一次并维护这个变量。

比如可以使用 LRU 算法记录最近使用的数据。

比如可以把已存在的文件使用 Map 记录路径和创建日期,并维护 Map 的数据,方便快速查找最早的文件。

比如可以对缓存的内容做加密解密处理等等,有时间我会继续优化。

哪些数据推荐用 File 缓存呢?

要知道 Cache 目录中的缓存可是随时可能被清空的,所以一些适合放在 SP 中的状态之类的值是不适合放入 File 缓存的。对于网络请求的缓存大家尽量在一些不会频繁变动的信息做缓存,比如一些很少变化的公告信息,全国省市区地址信息,行业信息,来源信息等等。也可以对一些对象做缓存,比如用户详情信息,附加信息等等。

并且有了自定义的文件缓存管理之后,我们可以扩展到其他的场景,比如网页的缓存,比如本地服务等。

当然了我知道大家大部分都是使用 dio 的,其实也是一样的用法,改巴改巴就能用了。

本文代码对应网络请求的具体 httpCode,与 Apicode 封装是跟业务关联,跟后端服务器的标准定义相关,并不适用于每一个项目。所以部分解析与封装代码并没有贴出来,其他的核心代码都已经贴出来了。

关于后续我也会持续分享一些实际开发中 Flutter 的踩坑与其他实现方案思路,有兴趣可以关注一下。

如果感觉本文对你有一点点点的启发,还望你能点赞支持一下,你的支持是我最大的动力啦。

Ok,这一期就此完结。

【Flutter】自定义文件缓存管理与网络请求的缓存策略

转载自:https://juejin.cn/post/7288998044019949583
评论
请登录