key-value 数据库在很多时候都是用来作为缓存使用的。而大部分 key-value 数据库都能配置缓存过期的时,但是在 Flutter 中常用的 shared_preferences 却没有相应的缓存过期策略,不过我们可以自己手动实现。

封装 shared_preferences

shared_preferences 本身使用起来已经非常方便了,所以只要进行简单的封装就可以,这里使用泛型的方式。

import 'package:shared_preferences/shared_preferences.dart';

class PrefUtil {
  late final SharedPreferencesWithCache _prefs;

  Future<void> initPref() async {
    _prefs = await SharedPreferencesWithCache.create(
        cacheOptions: const SharedPreferencesWithCacheOptions(allowList: <String>{
  
    }));
  }

  Future<void> reset() async {
    await _prefs.clear();
  }

  Future<void> setValue<T>(String key, T value) async {
    if (T == int) {
      await _prefs.setInt(key, value as int);
    } else if (T == bool) {
      await _prefs.setBool(key, value as bool);
    } else if (T == double) {
      await _prefs.setDouble(key, value as double);
    } else if (T == String) {
      await _prefs.setString(key, value as String);
    } else if (T == List<String>) {
      await _prefs.setStringList(key, value as List<String>);
    } else {
      throw ArgumentError('Unsupported type: $T');
    }
  }

  T? getValue<T>(String key) {
    if (T == int) {
      return _prefs.getInt(key) as T?;
    } else if (T == bool) {
      return _prefs.getBool(key) as T?;
    } else if (T == double) {
      return _prefs.getDouble(key) as T?;
    } else if (T == String) {
      return _prefs.getString(key) as T?;
    } else if (T == List<String>) {
      return _prefs.getStringList(key) as T?;
    } else {
      throw ArgumentError('Unsupported type: $T');
    }
  }

  Future<void> removeValue(String key) async {
    await _prefs.remove(key);
  }
}

需要注意的时,在 shared_preferences 的 2.3.0 版本之后,由于底层实现方式的改变,原有的 SharedPreferences 类型被替换为了 SharedPreferencesAsyncSharedPreferencesWithCache ,简单的说就是为了安全考虑,分为了异步调用,和同步调用,当使用 SharedPreferencesAsync 时,用于取值的 get 方法返回值是一个 Future ,也就是必须异步调用。而 SharedPreferencesWithCache 则仍可以使用同步调用的方式,和名字一样,原理是通过缓存的方式来实现的,出于安全考虑,现在要使用同步调用的话,必须要预设好 allowList ,也就是将来要存储数据的 key ,对不在列表中的 key 将会抛出异常。也就是说,如果你不知道将来要存储内容的 key 值是多少,就要使用 SharedPreferencesAsync 进行异步调用。

缓存控制类

其实实现的原理也非常简单,只要在存储原有数据的同时,记录下存储时的时间戳,下次取数据的时候,对比一下时间戳的差值,如果过期了,就可以进行进一步的操作。因为 shared_perferences 可以直接存储 List<String> ,所以我们直接将要存储的值放入一个列表中即可。

import 'dart:async';

import 'package:mood_diary/utils/utils.dart';

class CacheUtil {
  Future<List<String>?> getCacheList(String key, Future<List<String>?> Function() fetchData,
      {int maxAgeMillis = 900000}) async {
    var cachedData = Utils().prefUtil.getValue<List<String>>(key);
    // 检查缓存是否有效,如果无效则更新缓存
    if (cachedData == null || _isCacheExpired(cachedData, maxAgeMillis)) {
      await _updateCacheList(key, fetchData);

      cachedData = Utils().prefUtil.getValue<List<String>>(key); // 获取更新后的缓存数据
    }
    return cachedData;
  }

  bool _isCacheExpired(List<String> cachedData, int maxAgeMillis) {
    if (cachedData.length < 2) {
      return true; // 缓存数据格式不正确,视为过期
    }
    int timestamp = int.parse(cachedData.last);
    return DateTime.now().millisecondsSinceEpoch - timestamp >= maxAgeMillis;
  }

  Future<void> _updateCacheList(String key, Future<List<String>?> Function() fetchData) async {
    var newData = await fetchData();
    if (newData != null) {
      await Utils()
          .prefUtil
          .setValue<List<String>>(key, newData..add(DateTime.now().millisecondsSinceEpoch.toString()));
    }
  }
}

使用也非常简单,要取值时只需要调用 getCacheList 方法即可,fetchData 是一个函数,用于更新数据,这样就可以实现数据的自动更新。