精选文章

Flutter 缓存与离线

2025-03-25 · 工程化

缓存的目的不是“快一点”,而是稳定与体验一致:弱网不崩、离线可用、数据不乱。下面给出一套可直接复用的缓存与离线范式。

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 过期策略
  • 离线兜底标识
  • 缓存与用户态绑定
  • 清理机制

总结

缓存不是“可选优化”,而是稳定性工程。只要分层清晰、过期可控、离线兜底明确,用户体验会显著提升。

JJ

作者简介

专注于内容创作、产品策略与设计实践。欢迎交流合作。

上一篇 下一篇