Flutter 的缓存策略

原文 https://medium.com/@romaingre...

前言

在挪动应用程序中,缓存治理是一件十分重要的事件。

在本文中,我将告诉您如何在我的公司 Beapp 中设置策略缓存。

注释

W 怎么了?

如果你读了这篇文章,我想你晓得缓存是什么,然而以防万一..。

缓存基本上是将数据存储在设施的存储器中。

W 为什么应用缓存?

  • 如果用户连贯不好或者没有互联网
  • 限度 API 调用,特地是对于不须要常常刷新的数据
  • 存储敏感数据(咱们稍后探讨)

一张图片胜过一言半语:

Cache Strategy Scheme

缓存策略打算

如您所见,缓存的主要用途是始终尝试向用户显示数据。

对于敏感数据,出于以下起因,我将用户缓存与网络缓存拆散:

  • 网络缓存比用户缓存更短暂。
  • 相同,用户缓存存储敏感数据,如拜访令牌、刷新令牌,这些数据必须是平安的,用户不能拜访。
  • 更具体地说,刷新令牌的有效期可能很长(长达几个月) ,而经典数据可能在一小时后刷新,这将导致不必要的 API 调用。

因而,将这些策略拆散开来是一种很好的做法,即便它们能够被合并。

当初咱们理解了什么是缓存,让咱们深入研究代码吧!

H 如何建设这些策略?

文件树如下所示:

-- lib----- core------- cache--------- storage--------- strategy

在子文件夹存储中,咱们创立了一个文件 Storage.dart,其中蕴含一个抽象类 Storage

这个类是一个“契约 contrac”,咱们在其中申明操作数据的办法。

abstract class Storage {  Future<void> write(String key, String value);  Future<String?> read(String key);  Future<void> delete(String key);  Future<int> count({String? prefix});  Future<void> clear({String? prefix});}

正如我所说,咱们将通过咱们的应用程序操纵它们,但为此,咱们须要在设施中存储它们的办法。

咱们应用 Hive 包,它是一个基于键/值的存储解决方案。

总而言之,Hive 在设施的存储中创立了一个文件夹,您能够在其中存储一个 hiveBox,其中蕴含 key: value 数据。

咱们能够很容易地通过它的名字进入这个盒子。

当初咱们能够从 Storage 抽象类中实现这些办法。

class CacheStorage implements Storage {  static const _hiveBoxName = "cache";  CacheStorage()  {    Hive.initFlutter() ;  }  @override  Future<void> clear({String? prefix}) async {    final box = await Hive.openBox(_hiveBoxName);    if (prefix == null) {      await box.clear() ;    } else {      for (var key in box.keys) {        if (key is String && key.startsWith(prefix)) {          await box.delete(key);        }      }    }  }  @override  Future<void> delete(String key) async {    final box = await Hive.openBox(_hiveBoxName);    return box.delete(key);  }  @override  Future<String?> read(String key) async {    final box = await Hive.openBox(_hiveBoxName);    return box.get(key);  }  @override  Future<void> write(String key, String value) async {    final box = await Hive.openBox(_hiveBoxName);    return box.put(key, value);  }  @override  Future<int> count({String? prefix}) async {    final box = await Hive.openBox(_hiveBoxName);    if (prefix == null) {      return box.length;    } else {      var count = 0;      for (var key in box.keys) {        if (key is String && key.startsWith(prefix)) {          count++;        }      }      return count;    }  }}

准则很简略:

  • 咱们在创立 CacheStorage 时创立一个 hive 实例。
  • 每次咱们操作数据时,咱们将关上咱们的 Hive 框(应用它的名称)并执行触发的办法(获取、写入、删除...)。
  • 咱们能够很容易地通过它的键来拜访数据值。

当初咱们曾经有了操作数据的办法,咱们能够设置不同的策略,应用对立的调用语法来适应应用程序中的不同用例。

咱们开始创立一个契约缓存\_策略。缓存根中的 Dart 。该合同容许咱们利用其中一种策略并对其进行配置。

import 'dart:convert';import 'package:flutter/foundation.dart';import 'cache_manager.dart';import 'cache_wrapper.dart';import 'storage/storage.dart';abstract class CacheStrategy {  static const defaultTTLValue = 60 * 60 * 1000;  Future _storeCacheData<T>(String key, T value, Storage storage) async {    final cacheWrapper = CacheWrapper<T>(value, DateTime.now() .millisecondsSinceEpoch);    await storage.write(key, jsonEncode(cacheWrapper.toJsonObject() ));  }  _isValid<T>(CacheWrapper<T> cacheWrapper, bool keepExpiredCache, int ttlValue) => keepExpiredCache || DateTime.now() .millisecondsSinceEpoch < cacheWrapper.cachedDate + ttlValue;  Future<T> invokeAsync<T>(AsyncBloc<T> asyncBloc, String key, Storage storage) async {    final asyncData = await asyncBloc() ;    _storeCacheData(key, asyncData, storage);    return asyncData;  }  Future<T?> fetchCacheData<T>(String key, SerializerBloc serializerBloc, Storage storage, {bool keepExpiredCache = false, int ttlValue = defaultTTLValue}) async {    final value = await storage.read(key);    if (value != null) {      final cacheWrapper = CacheWrapper.fromJson(jsonDecode(value));      if (_isValid(cacheWrapper, keepExpiredCache, ttlValue)) {        if (kDebugMode) print("Fetch cache data for key $key: ${cacheWrapper.data}");        return serializerBloc(cacheWrapper.data);      }    }    return null;  }  Future<T?> applyStrategy<T>(AsyncBloc<T> asyncBloc, String key, SerializerBloc serializerBloc, int ttlValue, Storage storage);}
  • DefaultTTLValue 是存储在缓存中的数据的实时值。换句话说: 在这段时间之后,数据被认为是有效的。
    -\_storeCacheData() 通过 CacheWrapper 容许存储数据,咱们将在前面看到它。
    -\_isValid() 与 defaultTTLValue 相比,查看缓存获取是否依然无效
  • InvkeAsync() 将应用作为参数传递的 syncBloc 办法从近程地位(通常来自 Web 服务)获取数据,并存储和返回检索到的数据。
  • FetchCacheData() 将通过 key 参数从缓存中获取数据,转换 Cache Wrapper 接管到的 JSON 来查看它是否依然无效,如果无效,则返回具备相应类型的 Dart 对象中的序列化数据,这要感激 seralizerBloc。
  • ApplicyStrategy() 将执行要抉择的策略,其中蕴含所需的所有参数。

通过这些解释,咱们能够看到任何策略的施行门路:

  • 咱们调用 applicyStrategy() 来指出咱们想要利用哪个策略,以及所需的参数。
  • 要查看缓存的数据 fetchCacheData() ,该办法应用\_isValid() 查看有效性并返回数据或 null。
  • 为了从 WS 获取数据,咱们触发了 invekAsync() ,一旦接管到数据,就将它们与\_storeCacheData() 一起放到 cache 中。
class CacheWrapper<T> {  final T data;  final int cachedDate;  CacheWrapper(this.data, this.cachedDate);  CacheWrapper.fromJson(json)      : cachedDate = json['cachedDate'],        data = json['data'];  Map toJson()  => {'cachedDate': cachedDate, 'data': data};  @override  String toString()  => "CacheWrapper{cachedDate=$cachedDate, data=$data}";}

对于 CacheWrapper,您能够在根缓存文件夹中创立一个文件 cache_wrapper. dart。

正如其名称所示,CacheWrapper 是一个容许包装接收数据的类。它有两个参数,一个是容许包装任何类型数据的通用类型数据,另一个是在数据存储在缓存中的日期和工夫主动设置的 cachedDate。

From JSON() 和 toJson() 办法将接管到的数据转换为用于缓存的 JSON 或者在代码中应用它的 Map。

因而,能够将 CacheWrapper 解释为蕴含缓存数据并容许对这些数据进行编码/解码的“包装器”。

在本文的这个步骤中,咱们的构造文件夹如下所示:

-- lib----- core------- cache--------- storage----------- storage.dart----------- cache_storage.dart--------- cache_strategy.dart

当初咱们曾经看到了咱们的策略能够做什么的定义,让咱们深入研究它们的实现。

在缓存根目录中的新策略文件夹中,咱们将创立所有策略的文件。

每个策略都是单例的,所以应用程序中每个策略只有一个实例。

咱们能够应用 get_it 来注入咱们的策略,然而这减少了对包的依赖以及咱们所晓得的第三方的所有毛病,所以咱们本人创立了它们。

每个策略都将继承自形象的 CacheStrategy 类,它们将别离应用 applicyStrategy() 办法实现各自的策略。

AsyncOrCache

这个策略将首先调用端点来检索数据。如果抛出谬误(出于各种起因: 谬误 401,403,500...) ,咱们将检索存储在设施缓存中的最初数据。如果缓存中没有任何内容或有效数据,咱们将返回先前引发的谬误,以便在状态管理器中解决它(稍后将看到它)。

class AsyncOrCacheStrategy extends CacheStrategy {  static final AsyncOrCacheStrategy _instance = AsyncOrCacheStrategy._internal() ;  factory AsyncOrCacheStrategy()  {    return _instance;  }  AsyncOrCacheStrategy._internal() ;  @override  Future<T?> applyStrategy<T>(AsyncBloc<T> asyncBloc, String key, SerializerBloc<T> serializerBloc, int ttlValue, Storage storage) async => await invokeAsync(asyncBloc, key, storage).onError(        (RestException restError, stackTrace) async {          if (restError.code == 403 || restError.code == 404) {            storage.clear(prefix: key);            return Future.error(restError);          } else {            return await fetchCacheData(key, serializerBloc, storage, ttlValue: ttlValue) ?? Future.error(restError);          }        },      );}
CacheOrAsync

最初一个策略和前一个一样,只是反过来而已。首先,咱们检查数据是否存储在缓存中,如果后果为 null,则触发 WS 调用。如果抛出谬误,咱们在状态管理器中解决它。

class CacheOrAsyncStrategy extends CacheStrategy {  static final CacheOrAsyncStrategy _instance = CacheOrAsyncStrategy._internal() ;  factory CacheOrAsyncStrategy()  {    return _instance;  }  CacheOrAsyncStrategy._internal() ;  @override  Future<T?> applyStrategy<T>(AsyncBloc<T> asyncBloc, String key, SerializerBloc<T> serializerBloc, int ttlValue, Storage storage) async =>      await fetchCacheData(key, serializerBloc, storage, ttlValue: ttlValue) ?? await invokeAsync(asyncBloc, key, storage);}
只是同步

此策略调用 Web 服务来获取数据。

class JustAsyncStrategy extends CacheStrategy {  static final JustAsyncStrategy _instance = JustAsyncStrategy._internal() ;  factory JustAsyncStrategy()  {    return _instance;  }  JustAsyncStrategy._internal() ;  @override  Future<T?> applyStrategy<T>(AsyncBloc<T> asyncBloc, String key, SerializerBloc<T> serializerBloc, int ttlValue, Storage storage) async => await invokeAsync(asyncBloc, key, storage);}
JustCache
class JustCacheStrategy extends CacheStrategy {  static final JustCacheStrategy _instance = JustCacheStrategy._internal() ;  factory JustCacheStrategy()  {    return _instance;  }  JustCacheStrategy._internal() ;  @override  Future<T?> applyStrategy<T>(AsyncBloc<T> asyncBloc, String key, SerializerBloc<T> serializerBloc, int ttlValue, Storage storage) async =>      await fetchCacheData(key, serializerBloc, storage, ttlValue: ttlValue);}

此策略仅应用存储在设施缓存中的数据。毛病是如果应用程序找不到数据,则返回 null。

对于最初两种策略,它们能够间接由对缓存或网络的间接调用来代替,然而这里咱们保留了一种对立的调用形式。

当初咱们曾经看到了不同的策略,让咱们应用它们!

在根缓存文件夹中,咱们创立一个 cache_manager.dart 文件。

这个文件将蕴含构建缓存策略的所有逻辑。它将被间接注入到咱们的代码中(稍后我将回到这一点)。

import 'cache_strategy.dart';import 'storage/cache_storage.dart';typedef AsyncBloc<T> = Function;typedef SerializerBloc<T> = Function(dynamic);class CacheManager {  final CacheStorage cacheStorage;  CacheManager(    this.cacheStorage,  );  String? defaultSessionName;  StrategyBuilder from<T>(String key) => StrategyBuilder<T>(key, cacheStorage).withSession(defaultSessionName);  Future clear({String? prefix}) async {    if (defaultSessionName != null && prefix != null) {      await cacheStorage.clear(prefix: "${defaultSessionName}_$prefix");    } else if (prefix != null) {      await cacheStorage.clear(prefix: prefix);    } else if (defaultSessionName != null) {      await cacheStorage.clear(prefix: defaultSessionName);    } else {      await cacheStorage.clear() ;    }  }}class StrategyBuilder<T> {  final String _key;  final CacheStorage _cacheStorage;  StrategyBuilder(this._key, this._cacheStorage);  late AsyncBloc<T> _asyncBloc;  late SerializerBloc<T> _serializerBloc;  late CacheStrategy _strategy;  int _ttlValue = CacheStrategy.defaultTTLValue;  String? _sessionName;  StrategyBuilder withAsync(AsyncBloc<T> asyncBloc) {    _asyncBloc = asyncBloc;    return this;  }  StrategyBuilder withStrategy(CacheStrategy strategyType) {    _strategy = strategyType;    return this;  }  StrategyBuilder withTtl(int ttlValue) {    _ttlValue = ttlValue;    return this;  }  StrategyBuilder withSession(String? sessionName) {    _sessionName = sessionName;    return this;  }  StrategyBuilder withSerializer(SerializerBloc serializerBloc) {    _serializerBloc = serializerBloc;    return this;  }  String buildSessionKey(String key) => _sessionName != null ? "${_sessionName}_$key" : key;  Future<T?> execute()  async {    try {      return await _strategy.applyStrategy<T?>(_asyncBloc, buildSessionKey(_key), _serializerBloc, _ttlValue, _cacheStorage);    } catch (exception) {      rethrow;    }  }}

让我解释一下这个文件:

→ 它分为两个类: CacheManager 和 Strategies yBuilder

→ CacheManager 应用 from() 办法保留入口点。Strategies yBuilder 领有其余一些办法,这些办法容许咱们通过一些参数(如异步函数、序列化器等)来构建缓存会话。.

  • DefaultSessionName 容许咱们将一个全局名称放到将要关上的缓存会话中。例如,如果咱们为每个登录的用户创立一个缓存会话,咱们能够将用户的 firstName + lastName + id 设置为 defaultSessionName,这样咱们就能够应用这个名称轻松地操作整个缓存会话。
  • From() : 该办法创立一个通用类型 < T > 的 Strategies yBuilder 实例,该实例容许返回任何类型: List、 String、 Object... 一个键参数被传递,它将在 buildSessionKey() 办法中用于 hive 框的名称。AcheStorage 实例也作为参数传递,以便 Strategies yBuilder 能够应用它并将其传递给 CacheStrategy。最初,Strategies yBuilder 的 withSession() 办法用于命名以后缓存会话。
  • Clear() : 容许以不同形式革除缓存。咱们能够应用 Strategy Builder 的 defaultSessionName 或前缀参数清理缓存会话,或者清理创立的所有缓存。

一旦调用 from() 办法,就轮到调用 Strategies yBuilder 办法了:

  • With Async() : 咱们为构建器提供 AsyncBloc < T > 函数,构建器将从近程源(比方 API)获取数据。
  • WithSerializer() : 咱们为构建器提供序列化器/反序列化器,它负责将接管到的 JSON 数据转换为 dart 对象,反之亦然,应用 SerializerBloc < T > 函数。
因为 Dart 中的默认序列化/反序列化没有针对简单对象进行优化,因而 Flutter 倡议应用一个包(json_seralizable)。它将为每个 DTO 主动生成办法,而后将这些办法间接注入 seralizerBloc,用于序列化从缓存接管的数据。
  • WithTtl() : 为缓存提供生存工夫,默认状况下咱们将其设置为 1 小时。
  • WithStrategy() : 接管所抉择的策略单例。间接注入一个单例模式容许定制/增加不同的策略,例如,它比枚举更灵便。
  • Execute() : 后一个办法触发 applicyStrategy() 办法来执行缓存策略。

H 如何应用这种策略?

当初咱们曾经理解了这个实践,让咱们来看看在应用程序中实现缓存策略的理论状况。

我向你保障,这是最简略的局部。

首先,咱们须要注入咱们创立的 CacheManager。为了做到这一点,咱们应用 get_it 包,它将应用依赖注入来创立一个能够在整个代码库中应用的单例模式。

我建议您在应用程序的外围文件夹中创立一个 service_locator. dart 文件。

final getIt = GetIt.instance;void setupGetIt()  {  // Cache  getIt.registerSingleton<CacheManager>(CacheManager(CacheStorage() ));}

因而,咱们应用 CacheManager 来管理策略并保留 CacheStorage 实例用于存储。

这个 setupGetIt() 办法将在 app root starter 中触发,以注入 CacheManager 单实例。

当咱们尝试在 简洁我的项目架构 clean architecture 中工作时,咱们的故障看起来是这样的:

-- data----- datasource----- domain----- dto----- repository

咱们最感兴趣的是存储库文件夹,因为它在从数据源接管输出 dto 时充当网关,将其转换为来自域的实体。

让咱们以一个应用程序为例,它将显示学生要实现的工作。 咱们须要一个办法来检索调配。

class HomeworkAssignmentRepository {  final apiProvider = getIt<HomeworkDataSource>() ;  final _cacheManager = getIt<CacheManager>() ;  Future<List<HomeworkEntity>?> getHomeworkAssignment(String courseId, String studentId) async {    final List<HomeworkDto>? result = await _cacheManager        .from<List<HomeworkDto>>("homework-assignment-$courseId-$studentId")        .withSerializer((result) => HomeworkDto.fromJson(result))        .withAsync(()  => apiProvider.fetchHomeworkAssignment(courseId, studentId))        .withStrategy(AsyncOrCache() )        .execute() ;    if (result != null) {      return List<HomeworkEntity>.from(result.map((dto) => dto.toEntity() ));    }    return null;  }}

首先,咱们将咱们的 HomeworkDataSource 和 CacheManager 注入 get_it。

数据源将用于调用端点,管理器用于配置策略。

在未来的 getHomeworkAsmission 中,咱们心愿失去一个 HomeworkD 的列表,它将在 HomeworkEntity 中被转换。咱们看到咱们的策略失去了利用,咱们解释道:

  • From() 设置将应用哪个 dto 并给出缓存的密钥。
  • WithSerializer() 注入将反序列化数据的办法。
  • WithAsync() 注入带有必要参数的 API 调用。
  • WithStrategy() 容许定义要抉择的策略。
  • Execute() 将通过将定义的参数发送给 Strategies yBuilder 来触发咱们的策略。

当初,有了这个配置,咱们的策略将首先触发一个 API 调用来从服务器检索数据。如果调用引发谬误,则该策略将尝试从缓存中检索数据,最初,它将向 UI 返回数据(无论是否为陈腐数据)或 null。

结束语

如果本文对你有帮忙,请转发让更多的敌人浏览。

兴许这个操作只有你 3 秒钟,对我来说是一个激励,感激。

祝你有一个美妙的一天~


© 猫哥

  • 微信 ducafecat
  • https://wiki.ducafecat.tech
  • https://video.ducafecat.tech

本文由mdnice多平台公布