MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Flutter 网络请求中的数据缓存策略(结合 http/dio)

2024-10-297.7k 阅读

Flutter 网络请求与数据缓存概述

在 Flutter 应用开发中,网络请求是获取数据的常见操作。然而,频繁的网络请求不仅可能消耗用户的流量,还可能导致性能问题,尤其是在网络不稳定的情况下。为了解决这些问题,引入数据缓存策略是非常有必要的。数据缓存可以减少不必要的网络请求,提高应用的响应速度和用户体验。

在 Flutter 中,有多种网络请求库可供选择,其中 http 是官方提供的基础网络请求库,而 dio 是一个功能丰富且强大的第三方网络请求库,它在 http 的基础上进行了封装和扩展,提供了诸如拦截器、请求取消等实用功能。在本文中,我们将以 httpdio 为例,探讨如何实现数据缓存策略。

基于 http 库的简单缓存实现

http 库是 Flutter 官方提供的用于发送 HTTP 请求的库,它提供了简单易用的接口来进行网络通信。我们可以通过在本地存储请求结果来实现简单的数据缓存。

缓存数据的存储

我们可以使用 shared_preferences 库来在本地存储缓存数据。shared_preferences 允许我们在应用的本地存储中保存简单的数据类型,如字符串、整数、布尔值等。首先,在 pubspec.yaml 文件中添加依赖:

dependencies:
  shared_preferences: ^2.0.15

然后,在代码中导入并使用该库:

import 'package:shared_preferences/shared_preferences.dart';

接下来,我们定义一个函数来将数据存储到 shared_preferences 中:

Future<void> saveCache(String key, String value) async {
  final prefs = await SharedPreferences.getInstance();
  await prefs.setString(key, value);
}

以及一个函数来从 shared_preferences 中读取缓存数据:

Future<String?> getCache(String key) async {
  final prefs = await SharedPreferences.getInstance();
  return prefs.getString(key);
}

结合 http 请求实现缓存

假设我们要发送一个 GET 请求获取数据,并在本地缓存结果。我们可以这样实现:

import 'package:http/http.dart' as http;
import 'package:shared_preferences/shared_preferences.dart';

Future<String> fetchDataWithCache(String url) async {
  // 尝试从缓存中获取数据
  String? cachedData = await getCache(url);
  if (cachedData != null) {
    return cachedData;
  }

  // 如果缓存中没有数据,则发送网络请求
  final response = await http.get(Uri.parse(url));
  if (response.statusCode == 200) {
    // 保存数据到缓存
    await saveCache(url, response.body);
    return response.body;
  } else {
    throw Exception('Failed to load data');
  }
}

在上述代码中,fetchDataWithCache 函数首先尝试从缓存中获取数据。如果缓存中有数据,则直接返回。否则,发送网络请求,获取数据后将其保存到缓存中并返回。

使用 dio 库实现更高级的缓存策略

dio 库提供了更多的功能和灵活性,使得我们可以实现更复杂和高级的数据缓存策略。

dio 基本使用

首先,在 pubspec.yaml 文件中添加 dio 依赖:

dependencies:
  dio: ^5.1.0

然后,在代码中导入并使用 dio

import 'package:dio/dio.dart';

以下是一个简单的 dio GET 请求示例:

void fetchDataWithDio() async {
  try {
    Dio dio = Dio();
    Response response = await dio.get('https://example.com/api/data');
    print(response.data);
  } catch (e) {
    print('Error: $e');
  }
}

缓存拦截器

为了实现缓存功能,我们可以利用 dio 的拦截器。拦截器可以在请求发送前和响应返回后进行一些操作。我们定义一个缓存拦截器如下:

import 'package:dio/dio.dart';
import 'package:shared_preferences/shared_preferences.dart';

class CacheInterceptor extends Interceptor {
  @override
  void onResponse(Response response, ResponseInterceptorHandler handler) {
    // 缓存响应数据
    _saveCache(response.requestOptions.uri.toString(), response.data.toString());
    super.onResponse(response, handler);
  }

  @override
  Future<void> onRequest(RequestOptions options, RequestInterceptorHandler handler) async {
    // 尝试从缓存中获取数据
    String? cachedData = await _getCache(options.uri.toString());
    if (cachedData != null) {
      // 如果缓存中有数据,直接返回缓存数据
      handler.resolve(Response(
        requestOptions: options,
        data: cachedData,
        statusCode: 200,
      ));
    } else {
      // 如果缓存中没有数据,继续正常的请求流程
      super.onRequest(options, handler);
    }
  }

  Future<void> _saveCache(String key, String value) async {
    final prefs = await SharedPreferences.getInstance();
    await prefs.setString(key, value);
  }

  Future<String?> _getCache(String key) async {
    final prefs = await SharedPreferences.getInstance();
    return prefs.getString(key);
  }
}

在上述代码中,CacheInterceptor 类继承自 Interceptor。在 onResponse 方法中,当收到响应时,我们将响应数据保存到缓存中。在 onRequest 方法中,当发起请求时,我们首先尝试从缓存中获取数据,如果缓存中有数据,则直接返回缓存数据,否则继续正常的请求流程。

使用缓存拦截器

要使用我们定义的缓存拦截器,我们需要在 Dio 实例中添加该拦截器:

void fetchDataWithCachedDio() async {
  Dio dio = Dio();
  dio.interceptors.add(CacheInterceptor());
  try {
    Response response = await dio.get('https://example.com/api/data');
    print(response.data);
  } catch (e) {
    print('Error: $e');
  }
}

通过以上方式,我们利用 dio 的拦截器实现了一个简单的缓存策略。当发起请求时,首先检查缓存中是否有数据,如果有则直接返回,否则发送网络请求并在收到响应后将数据缓存起来。

缓存策略的优化

缓存有效期控制

上述简单的缓存策略没有考虑缓存数据的有效期。在实际应用中,我们可能需要设置缓存数据的有效期,以确保数据的时效性。我们可以通过在缓存数据时记录时间戳来实现这一点。

在保存缓存数据时,同时保存一个时间戳:

Future<void> saveCacheWithTimestamp(String key, String value) async {
  final prefs = await SharedPreferences.getInstance();
  final timestamp = DateTime.now().millisecondsSinceEpoch;
  await prefs.setString('$key_timestamp', timestamp.toString());
  await prefs.setString(key, value);
}

在获取缓存数据时,检查时间戳是否过期:

Future<String?> getCacheWithExpiry(String key, int cacheDurationInSeconds) async {
  final prefs = await SharedPreferences.getInstance();
  String? cachedData = prefs.getString(key);
  String? timestampStr = prefs.getString('$key_timestamp');
  if (cachedData != null && timestampStr != null) {
    final timestamp = int.parse(timestampStr);
    final now = DateTime.now().millisecondsSinceEpoch;
    if (now - timestamp < cacheDurationInSeconds * 1000) {
      return cachedData;
    }
  }
  return null;
}

修改缓存拦截器以支持缓存有效期控制:

class CacheInterceptorWithExpiry extends Interceptor {
  final int cacheDurationInSeconds;

  CacheInterceptorWithExpiry(this.cacheDurationInSeconds);

  @override
  void onResponse(Response response, ResponseInterceptorHandler handler) {
    _saveCacheWithTimestamp(response.requestOptions.uri.toString(), response.data.toString());
    super.onResponse(response, handler);
  }

  @override
  Future<void> onRequest(RequestOptions options, RequestInterceptorHandler handler) async {
    String? cachedData = await _getCacheWithExpiry(options.uri.toString(), cacheDurationInSeconds);
    if (cachedData != null) {
      handler.resolve(Response(
        requestOptions: options,
        data: cachedData,
        statusCode: 200,
      ));
    } else {
      super.onRequest(options, handler);
    }
  }

  Future<void> _saveCacheWithTimestamp(String key, String value) async {
    final prefs = await SharedPreferences.getInstance();
    final timestamp = DateTime.now().millisecondsSinceEpoch;
    await prefs.setString('$key_timestamp', timestamp.toString());
    await prefs.setString(key, value);
  }

  Future<String?> _getCacheWithExpiry(String key, int cacheDurationInSeconds) async {
    final prefs = await SharedPreferences.getInstance();
    String? cachedData = prefs.getString(key);
    String? timestampStr = prefs.getString('$key_timestamp');
    if (cachedData != null && timestampStr != null) {
      final timestamp = int.parse(timestampStr);
      final now = DateTime.now().millisecondsSinceEpoch;
      if (now - timestamp < cacheDurationInSeconds * 1000) {
        return cachedData;
      }
    }
    return null;
  }
}

在使用时,我们可以根据需要设置缓存有效期:

void fetchDataWithCachedDioAndExpiry() async {
  Dio dio = Dio();
  dio.interceptors.add(CacheInterceptorWithExpiry(3600)); // 缓存有效期1小时
  try {
    Response response = await dio.get('https://example.com/api/data');
    print(response.data);
  } catch (e) {
    print('Error: $e');
  }
}

缓存数据的更新策略

在某些情况下,我们可能需要在数据发生变化时及时更新缓存。例如,当用户进行了某些操作导致服务器数据更新时,我们需要确保本地缓存也相应更新。一种常见的做法是在服务器端返回一些标识数据版本的信息,客户端在收到响应时,根据版本信息判断是否需要更新缓存。

假设服务器在响应头中返回一个 data - version 字段,我们可以在缓存拦截器中进行如下处理:

class CacheInterceptorWithVersion extends Interceptor {
  @override
  void onResponse(Response response, ResponseInterceptorHandler handler) {
    final version = response.headers.value('data - version');
    if (version != null) {
      _saveCacheWithVersion(response.requestOptions.uri.toString(), response.data.toString(), version);
    }
    super.onResponse(response, handler);
  }

  @override
  Future<void> onRequest(RequestOptions options, RequestInterceptorHandler handler) async {
    String? cachedData = await _getCacheWithVersion(options.uri.toString());
    if (cachedData != null) {
      handler.resolve(Response(
        requestOptions: options,
        data: cachedData,
        statusCode: 200,
      ));
    } else {
      super.onRequest(options, handler);
    }
  }

  Future<void> _saveCacheWithVersion(String key, String value, String version) async {
    final prefs = await SharedPreferences.getInstance();
    await prefs.setString('$key_version', version);
    await prefs.setString(key, value);
  }

  Future<String?> _getCacheWithVersion(String key) async {
    final prefs = await SharedPreferences.getInstance();
    String? cachedData = prefs.getString(key);
    String? cachedVersion = prefs.getString('$key_version');
    // 在实际应用中,这里应该发送一个轻量级的请求到服务器获取最新版本号
    // 假设这里有一个函数 getServerVersion 来获取服务器版本号
    String serverVersion = await getServerVersion(key);
    if (cachedData != null && cachedVersion == serverVersion) {
      return cachedData;
    }
    return null;
  }
}

在上述代码中,CacheInterceptorWithVersion 类在 onResponse 方法中保存数据时,同时保存数据的版本号。在 onRequest 方法中,获取缓存数据时,通过比较本地缓存的版本号和服务器最新的版本号来决定是否使用缓存数据。

缓存数据的管理与清理

随着应用的使用,缓存数据可能会占用越来越多的存储空间。因此,我们需要考虑缓存数据的管理与清理策略。

手动清理缓存

我们可以提供一个接口让用户手动清理缓存。例如,在应用的设置页面中添加一个“清除缓存”按钮,当用户点击该按钮时,调用以下方法:

Future<void> clearCache() async {
  final prefs = await SharedPreferences.getInstance();
  await prefs.clear();
}

自动清理缓存

除了手动清理,我们还可以实现自动清理缓存的策略。例如,当缓存数据的大小超过一定阈值时,自动清理最旧的缓存数据。我们可以通过记录每个缓存数据的大小,并维护一个缓存数据的列表来实现这一点。

首先,修改保存缓存数据的方法,同时记录数据大小:

Future<void> saveCacheWithSize(String key, String value) async {
  final prefs = await SharedPreferences.getInstance();
  final size = value.length;
  await prefs.setString('$key_size', size.toString());
  await prefs.setString(key, value);
  await _updateCacheList(key);
}

然后,维护一个缓存数据列表,并在保存和读取缓存数据时更新该列表:

Future<void> _updateCacheList(String key) async {
  final prefs = await SharedPreferences.getInstance();
  List<String>? cacheList = prefs.getStringList('cache_list');
  if (cacheList == null) {
    cacheList = [];
  }
  if (!cacheList.contains(key)) {
    cacheList.add(key);
    await prefs.setStringList('cache_list', cacheList);
  }
}

Future<List<String>> _getCacheList() async {
  final prefs = await SharedPreferences.getInstance();
  return prefs.getStringList('cache_list') ?? [];
}

最后,实现自动清理缓存的方法:

Future<void> autoClearCache(int maxCacheSize) async {
  final prefs = await SharedPreferences.getInstance();
  List<String> cacheList = await _getCacheList();
  int totalSize = 0;
  for (String key in cacheList) {
    String? sizeStr = prefs.getString('$key_size');
    if (sizeStr != null) {
      totalSize += int.parse(sizeStr);
    }
  }
  while (totalSize > maxCacheSize && cacheList.isNotEmpty) {
    String oldestKey = cacheList.first;
    await prefs.remove(oldestKey);
    await prefs.remove('$oldestKey_size');
    cacheList.removeAt(0);
    await prefs.setStringList('cache_list', cacheList);
    String? sizeStr = prefs.getString('$oldestKey_size');
    if (sizeStr != null) {
      totalSize -= int.parse(sizeStr);
    }
  }
}

在实际应用中,可以在合适的时机(如应用启动时或定期检查)调用 autoClearCache 方法来自动清理缓存。

缓存策略在不同场景下的应用

列表数据缓存

在应用中,列表数据是常见的展示内容。对于列表数据的缓存,我们可以根据列表的分页情况进行缓存。例如,假设我们有一个分页获取列表数据的接口,我们可以为每个分页的请求单独缓存数据。

假设请求接口为 https://example.com/api/list?page=1&limit=10,我们可以在缓存拦截器中根据请求的 pagelimit 参数来缓存数据:

class ListCacheInterceptor extends Interceptor {
  @override
  void onResponse(Response response, ResponseInterceptorHandler handler) {
    final uri = response.requestOptions.uri;
    final page = uri.queryParameters['page'];
    final limit = uri.queryParameters['limit'];
    final key = 'list_$page\_$limit';
    _saveCache(key, response.data.toString());
    super.onResponse(response, handler);
  }

  @override
  Future<void> onRequest(RequestOptions options, RequestInterceptorHandler handler) async {
    final uri = options.uri;
    final page = uri.queryParameters['page'];
    final limit = uri.queryParameters['limit'];
    final key = 'list_$page\_$limit';
    String? cachedData = await _getCache(key);
    if (cachedData != null) {
      handler.resolve(Response(
        requestOptions: options,
        data: cachedData,
        statusCode: 200,
      ));
    } else {
      super.onRequest(options, handler);
    }
  }

  Future<void> _saveCache(String key, String value) async {
    final prefs = await SharedPreferences.getInstance();
    await prefs.setString(key, value);
  }

  Future<String?> _getCache(String key) async {
    final prefs = await SharedPreferences.getInstance();
    return prefs.getString(key);
  }
}

在使用时,将该拦截器添加到 Dio 实例中:

void fetchListWithCache() async {
  Dio dio = Dio();
  dio.interceptors.add(ListCacheInterceptor());
  try {
    Response response = await dio.get('https://example.com/api/list?page=1&limit=10');
    print(response.data);
  } catch (e) {
    print('Error: $e');
  }
}

这样,当再次请求相同分页的列表数据时,将直接从缓存中获取,提高了加载速度。

详情页数据缓存

对于详情页数据,由于其数据量相对较小且更新频率可能较低,我们可以采用较长的缓存有效期。同时,我们可以根据详情页的唯一标识(如 ID)来缓存数据。

假设详情页的请求接口为 https://example.com/api/detail?id=123,我们可以这样实现缓存:

class DetailCacheInterceptor extends Interceptor {
  @override
  void onResponse(Response response, ResponseInterceptorHandler handler) {
    final uri = response.requestOptions.uri;
    final id = uri.queryParameters['id'];
    final key = 'detail_$id';
    _saveCache(key, response.data.toString());
    super.onResponse(response, handler);
  }

  @override
  Future<void> onRequest(RequestOptions options, RequestInterceptorHandler handler) async {
    final uri = options.uri;
    final id = uri.queryParameters['id'];
    final key = 'detail_$id';
    String? cachedData = await _getCache(key);
    if (cachedData != null) {
      handler.resolve(Response(
        requestOptions: options,
        data: cachedData,
        statusCode: 200,
      ));
    } else {
      super.onRequest(options, handler);
    }
  }

  Future<void> _saveCache(String key, String value) async {
    final prefs = await SharedPreferences.getInstance();
    await prefs.setString(key, value);
  }

  Future<String?> _getCache(String key) async {
    final prefs = await SharedPreferences.getInstance();
    return prefs.getString(key);
  }
}

在使用时,添加该拦截器到 Dio 实例:

void fetchDetailWithCache() async {
  Dio dio = Dio();
  dio.interceptors.add(DetailCacheInterceptor());
  try {
    Response response = await dio.get('https://example.com/api/detail?id=123');
    print(response.data);
  } catch (e) {
    print('Error: $e');
  }
}

通过这种方式,当用户再次访问相同 ID 的详情页时,将从缓存中快速获取数据,提升用户体验。

缓存策略的性能分析与调优

缓存命中率分析

缓存命中率是衡量缓存策略有效性的重要指标。缓存命中率指的是缓存中存在请求数据的次数与总请求次数的比例。我们可以通过统计缓存命中和未命中的次数来计算缓存命中率。

在缓存拦截器中添加统计逻辑:

class CacheInterceptorWithStats extends Interceptor {
  int cacheHitCount = 0;
  int cacheMissCount = 0;

  @override
  void onResponse(Response response, ResponseInterceptorHandler handler) {
    _saveCache(response.requestOptions.uri.toString(), response.data.toString());
    super.onResponse(response, handler);
  }

  @override
  Future<void> onRequest(RequestOptions options, RequestInterceptorHandler handler) async {
    String? cachedData = await _getCache(options.uri.toString());
    if (cachedData != null) {
      cacheHitCount++;
      handler.resolve(Response(
        requestOptions: options,
        data: cachedData,
        statusCode: 200,
      ));
    } else {
      cacheMissCount++;
      super.onRequest(options, handler);
    }
  }

  Future<void> _saveCache(String key, String value) async {
    final prefs = await SharedPreferences.getInstance();
    await prefs.setString(key, value);
  }

  Future<String?> _getCache(String key) async {
    final prefs = await SharedPreferences.getInstance();
    return prefs.getString(key);
  }

  double getCacheHitRate() {
    if (cacheHitCount + cacheMissCount == 0) {
      return 0;
    }
    return cacheHitCount / (cacheHitCount + cacheMissCount);
  }
}

在应用中,我们可以在合适的时机(如应用关闭时)打印缓存命中率:

void main() async {
  Dio dio = Dio();
  CacheInterceptorWithStats cacheInterceptor = CacheInterceptorWithStats();
  dio.interceptors.add(cacheInterceptor);
  // 进行一系列网络请求
  await dio.get('https://example.com/api/data1');
  await dio.get('https://example.com/api/data2');
  //...
  print('Cache Hit Rate: ${cacheInterceptor.getCacheHitRate()}');
}

通过分析缓存命中率,我们可以了解缓存策略的效果。如果缓存命中率较低,可能需要调整缓存策略,如延长缓存有效期、优化缓存键的生成等。

缓存对应用性能的影响

虽然缓存可以提高应用的响应速度,但不合理的缓存策略也可能对应用性能产生负面影响。例如,如果缓存数据量过大,可能会占用过多的内存,导致应用性能下降。此外,如果缓存更新不及时,可能会导致用户看到的数据不准确。

为了避免这些问题,我们需要在缓存数据量和缓存更新频率之间找到平衡。可以通过设置合理的缓存有效期、定期清理缓存数据等方式来优化缓存对应用性能的影响。同时,在设计缓存策略时,要充分考虑应用的业务需求和数据变化特点,确保缓存策略既能提高性能,又能保证数据的准确性。

在 Flutter 网络请求中,合理的缓存策略对于提高应用性能和用户体验至关重要。通过结合 httpdio 库,并灵活运用各种缓存优化技巧,我们可以实现高效、可靠的数据缓存机制,满足不同应用场景的需求。在实际开发中,需要根据应用的特点和业务需求,不断调整和优化缓存策略,以达到最佳的性能和用户体验。