缓存的目的不是“快一点”,而是稳定与体验一致:弱网不崩、离线可用、数据不乱。下面给出一套可直接复用的缓存与离线范式。
1. 缓存分层:内存 + 磁盘
推荐把缓存拆成两层:
- 内存缓存:快,但易丢失
- 磁盘缓存:慢一些,但持久
先定义统一接口:
abstract class CacheStore {
Future<void> write(String key, String value, {Duration? ttl});
Future<String?> read(String key);
Future<void> delete(String key);
}
2. 内存缓存(带过期)
class MemoryCache implements CacheStore {
final _map = <String, _Entry>{};
@override
Future<void> write(String key, String value, {Duration? ttl}) async {
_map[key] = _Entry(value, ttl != null ? DateTime.now().add(ttl) : null);
}
@override
Future<String?> read(String key) async {
final entry = _map[key];
if (entry == null) return null;
if (entry.expireAt != null && DateTime.now().isAfter(entry.expireAt!)) {
_map.remove(key);
return null;
}
return entry.value;
}
@override
Future<void> delete(String key) async {
_map.remove(key);
}
}
class _Entry {
final String value;
final DateTime? expireAt;
_Entry(this.value, this.expireAt);
}
3. 磁盘缓存(SharedPreferences 示例)
class DiskCache implements CacheStore {
final SharedPreferences prefs;
DiskCache(this.prefs);
@override
Future<void> write(String key, String value, {Duration? ttl}) async {
final expireAt = ttl != null ? DateTime.now().add(ttl).millisecondsSinceEpoch : null;
final payload = jsonEncode({"v": value, "e": expireAt});
await prefs.setString(key, payload);
}
@override
Future<String?> read(String key) async {
final raw = prefs.getString(key);
if (raw == null) return null;
final data = jsonDecode(raw) as Map<String, dynamic>;
final expireAt = data["e"] as int?;
if (expireAt != null && DateTime.now().millisecondsSinceEpoch > expireAt) {
await prefs.remove(key);
return null;
}
return data["v"] as String?;
}
@override
Future<void> delete(String key) async {
await prefs.remove(key);
}
}
4. 组合缓存:先内存再磁盘
class CacheRepository {
final MemoryCache memory;
final DiskCache disk;
CacheRepository(this.memory, this.disk);
Future<void> write(String key, String value, {Duration? ttl}) async {
await memory.write(key, value, ttl: ttl);
await disk.write(key, value, ttl: ttl);
}
Future<String?> read(String key) async {
final mem = await memory.read(key);
if (mem != null) return mem;
final diskValue = await disk.read(key);
if (diskValue != null) {
await memory.write(key, diskValue);
}
return diskValue;
}
}
5. 离线兜底:读缓存 + 标记状态
业务层不要直接失败,而是:
- 请求失败时读缓存
- UI 显示“离线数据”标识
Future<Result<User>> loadUser() async {
try {
final res = await api.get('/user');
await cache.write('user', jsonEncode(res));
return Result.success(res, stale: false);
} catch (_) {
final cached = await cache.read('user');
if (cached != null) {
return Result.success(jsonDecode(cached), stale: true);
}
return Result.failure();
}
}
6. 一致性策略:避免“脏数据”
- 对重要数据设置短 TTL
- 用户主动刷新时强制请求
- 缓存必须绑定用户态(如 userId)
示例:
final key = 'user_${userId}_profile';
7. 清理策略
- 启动时清理过期
- 设置最大容量
- 对大对象使用文件缓存
8. 实践清单
- 内存 + 磁盘双层缓存
- 统一 TTL 过期策略
- 离线兜底标识
- 缓存与用户态绑定
- 清理机制
总结
缓存不是“可选优化”,而是稳定性工程。只要分层清晰、过期可控、离线兜底明确,用户体验会显著提升。