ورود و عضویت
0
سبد خرید شما خالی است
0
سبد خرید شما خالی است

پیاده سازی قابلیت کش Cache اطلاعات در فلاتر

0 دیدگاه

پیاده سازی قابلیت کش ( Cache ) کردن اطلاعات باعث میشود کاربر در هنگام استفاده از اپلیکیشن تجربه بسیار بهتری داشته باشد.

کش کردن اطلاعات بصورت کلی به معنی ذخیره سازی موقت اطلاعات گوناگون بروی سیستم کاربر میباشد که در اینجا میتواند گوشی موبایل یا یک کامپیوتر باشد.

این قابلیت باعث میشود در هنگام استفاده از برنامه کاربر سرعت بیشتری را تجربه کند و از دریافت اطلاعات تکراری

جلوگیری میشود.

در این مقاله قصد داریم تا به پیاده سازی قابلیت کش کردن اطلاعات در فریمورک فلاتر بپردازیم.

برای استفاده از این مقاله نیاز به آشنایی با مباحث پیشرفته در فلاتر دارید. اگر نیاز به آموزش های مقدماتی تا پیشرفته دارید از دوره استادی فلاتر استفاده کنید.

چرا از Cache باید استفاده کرد؟

همانطور که گفتیم کش کردن اطلاعات به معنی ذخیره سازی داده ها میباشد. در این حالت اطلاعات یکبار دریافت و ذخیره میشوند و در دفعات این اطلاعات از روی حافظه خوانده میشود.

در مواردی مختلفی اطلاعات Cache شده برای کاربر بسیار کمک کننده میباشد مانند:

  • در هنگام قطع یا ضعیف بودن اتصال اینترنت
  • محدودیت ارسال درخواست به API, مخصوصا زمانی که اطلاعات نیازی به بروزرسانی مداوم ندارند.
  • ذخیره سازی اطلاعات حساس
استراتژی کش اطلاعات

در تصویر بالا همانطور که مشاهده میکنید هدف از کش کردن اطلاعات نمایش دائمی اطلاعات به کاربر در شرایط مختلف میباشد.

با توجه به حساس بودن بعضی از اطلاعات در این روش قصد داریم تا سیستم Cache شبکه و کاربر را از یکدیگر جدا کنیم. دلیل اینکار نیز موارد زیر میباشد:

  • اطلاعات شبکه مانند مطالب, تصاویر و… زودگذر تر از اطلاعات کاربر میباشد.
  • اطلاعات کاربری شامل کلید های احراز هویت و باقی مسائل حساس میباشد بنابراین نباید به راحتی در دسترس افراد قرار گیرد.
  • اطلاعات توکن مثل RefreshToken چرخه عمر طولانی دارند و گاهی اوقات تا چند ماه ممکن است ثابت بماند. برخلاف اطلاعات عادی که شاید هر چند ساعت تغییر کنند.

بنابراین برای پیاده سازی Cache در فلاتر این دو بخش را از همدیگر جدا میکنیم هرچند شاید بتوان آنها را باهم ترکیب کرد.

پیاده سازی سیستم Cache

ساختار درختی فایل هایی که نیاز داریم به شکل زیر میباشد.

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

درون پوشه storage فایلی به نام storage.dart ایجاد میکنیم که شامل Abstract کلاس Storage میباشد.

این کلاس شامل متدهایی برای مدیریت اطلاعات میباشد.

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 در حافظه دستگاه پوشه جدیدی ایجاد میکند که شامل فایل های ذخیره سازی شده به شکل key:value میباشد.

درون پوشه storage فایل جدیدی به نام cache_storage.dart ایجاد میکنیم. در این کلاس متدهای طراحی شده قبلی را پیاده سازی میکنیم.

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 تعریف کردیم.

هربار که قصد تغییر در اطلاعات را داشتیم متد متناظر با آن را فراخوانی میکنیم.

با استفاده از کلید به مقدار اطلاعات ذخیره شده دسترسی خواهیم داشت.

بعد از اینکه روشهای ذخیره سازی اطلاعات پیاده سازی کردیم به تعیین استراتژی کش کردن اطلاعات خواهیم پرداخت.

درون پوشه cache فایل جدیدی به نام cache_strategy.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(): معتبر بودن اطلاعات را بررسی میکند.

invokeAsync(): اطلاعات را از طریق منابع مختلف که عموما به وسیله وب سرویس میباشد دریافت میکند.

fetchCacheData(): اطلاعات Cache شده را از طریق کلید مورد نظر دریافت و ساختار JSON را نیز با کمک Cache Wrapper برای معتبر بودن بررسی میکند.

برای طراحی کلاس CacheWrapper فایل جدیدی به نام cache_wrapper.dart ایجاد میکنیم.

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 عملیات پیچیده ای را انجام نمیدهد و صرفا کار تبدیل اطلاعات به Json و برعکس را انجام میدهد.

در ادامه داخل پوشه strategy در ریشه cache استراتژی های مختلفی که نیاز داریم را پیاده سازی میکنیم.

تمام استراتژی ها از الگوی singleton پیروی میکنند.

تمام استراتژی های که پیاده سازی میکنیم از کلاس CacheStrategy ارث بری میکنند. این کلاس ها استراتژی کش کردن اطلاعات را به ترتیب با کمک متد applyStrategy() انجام میدهند.

استراتژی AsyncOrCache

در این نوع از استراتژی ابتدا برای نمایش اطلاعات به کاربر یک درخواست به سمت وب سرویس ارسال میشود و در صورتی که این عملیات با خطا مواجه شود آخرین اطلاعات ذخیره شده در حافظه گوشی را به کاربر نمایش میدهیم.

اگر اطلاعاتی در حافظه Cache وجود نداشت و یا آنها فاقد اعتبار بودند خطایی که در مرحله اول رخ داده بود را به کاربر نمایش میدهیم.

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

این استراتژی مشابه حالت قبل میباشد اما کاملا به شکل معکوس. در این حالت ابتدا حافظه کش بررسی میشود اگر فاقد اطلاعات معتبر بود یک درخواست به وب سرویس برای دریافت اطلاعات ارسال میشود و در صورت مواجه شدن با خطا پیامی متناظر با آن به کاربر نمایش داده میشود.

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

استراتژی JustCache

در این سناریو نمایش اطلاعات فقط از طریق حافظه کش انجام میشود. عیب بزرگ این روش در این است که اگر اطلاعاتی وجود نداشته باشد مقدار null بازگردانده میشود.

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

تا این مرحله استراتژی های مختلفی را مشاهده کردیم حالا نوبت به استفاده عملی از آنها میرسد.

در پوشه cache فایل جدیدی به نام 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 و StrategyBuilder میباشد.

CacheManager نقطه شروع کار میباشد که از متد from() آغاز میشود.

StrategyBuilder با کمک متدهایی که در اختیار دارد عملیات ذخیره سازی و دریافت اطلاعات را انجام میدهد.

defaultSessionName برای نام گذاری اطلاعات کش استفاده میشود.

چگونه از این استراتژی ها استفاده کنیم؟

تمام موارد تئوری که تا به اینجا بررسی کردیم را به شکل عملی داخل یک اپلیکیشن قرار هست پیاده سازی کنیم.

در ابتدا نیاز به انجام تزریق وابستگی های خود داریم که شامل تزریق کلاس CacheManager میباشد.

برای راحت تر انجام دادن این قسمت از پکیج get_it در فلاتر استفاده میکنیم.

داخل پوشه core فایل جدیدی به نام service_locator.dart ایجاد میکنیم.

final getIt = GetIt.instance;

void setupGetIt() {
  // Cache
  getIt.registerSingleton<CacheManager>(CacheManager(CacheStorage()));

}

متد setupGetIt() در هنگام آغاز برنامه اجرا میشود تا وابستگی موردنظر تزریق شود.

آموزش پکیج Freezed

از آنجایی که در این پروژه از معماری تمیز استفاده میکنیم ساختار پروژه ما به شکل زیر خواهد بود:

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

برای بخش آخر کار مثال زیر را بررسی میکنیم.

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;

  }
}

ابتدا با استفاده از get_it دو کلاس HomeworkDataSource و CacheManager را تزریق میکنیم.

از datasource برای فراخوانی وب سرویس و مدیریت استراتژی استفاده میشود.

در این سناریو ابتدا اطلاعات از طریق وب سرویس دریافت میشود و اگر برنامه با خطایی مواجه اطلاعات از طریق حافظه کش نمایش داده میشود.

دیدگاهتان را بنویسید

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *

Hesam
07 آوریل 2023
آموزش فارسی فلاتر
آموزش فارسی flutter