Flutter如何实现先读本地缓存再拉取API的异步数据加载展示
Flutter 缓存优先加载逻辑实现方案
你描述的「先加载本地缓存、后台拉取最新API数据、更新后落盘缓存」的逻辑,在Flutter里不需要强行套FutureBuilder——FutureBuilder更适合单次等待执行完成的场景,没法实现「本地缓存先渲染、网络数据静默更新」的分段渲染效果,用类React Hooks的写法或者原生StatefulWidget都可以直接落地,两种思路的具体实现如下:
前置准备
- 状态层:习惯React Hooks写法可以直接用
flutter_hooks包,提供和useState/useEffect几乎一致的API;不用第三方库的话,原生StatefulWidget的setState也能实现完全相同的逻辑 - 本地存储:简单键值对用
shared_preferences,结构化对象存储用hive/isar都可以 - 网络请求:用原生
http或者dio都不影响核心逻辑
第一种思路:主动拉取更新模式
对应你说的「读本地→拉API→更新状态+写缓存」流程,实现步骤:
- 用状态钩子存储页面数据,初始值为null
- 页面初始化时先读取本地存储:如果本地有缓存,立刻更新状态渲染,让用户第一时间看到内容
- 本地读取完成后,不管有没有缓存,都发起API请求拉取最新数据
- API返回成功后,更新页面状态,同时将新数据序列化写入本地存储
- 渲染逻辑:状态为null(本地无缓存、第一次加载)时展示
CircularProgressIndicator加载指示器,有数据时直接渲染内容
代码示例:
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'dart:convert'; class CacheFirstPage extends HookWidget { const CacheFirstPage({super.key}); // 替换成你实际的API请求逻辑 Future<Map<String, dynamic>> fetchApiData() async { await Future.delayed(const Duration(seconds: 1)); return {'content': '最新API返回的数据', 'updateTime': DateTime.now().toString()}; } @override Widget build(BuildContext context) { // 等价于React的useState,存储页面展示数据 final pageData = useState<Map<String, dynamic>?>(null); useEffect(() { // 等价于React useEffect(() => {}, []),页面初始化时执行一次 Future<void> loadData() async { final prefs = await SharedPreferences.getInstance(); // 第一步:读取本地缓存 final localCache = prefs.getString('page_cache_key'); if (localCache != null) { // 有缓存直接更新状态,用户无需等待网络 pageData.value = jsonDecode(localCache); } // 第二步:拉取最新API数据 final latestData = await fetchApiData(); // 第三步:更新页面状态 + 新数据写入本地缓存 pageData.value = latestData; await prefs.setString('page_cache_key', jsonEncode(latestData)); } loadData(); return null; }, const []); return Scaffold( appBar: AppBar(title: const Text('缓存优先页面')), body: Center( child: pageData.value == null ? const CircularProgressIndicator() : Padding( padding: const EdgeInsets.all(16), child: Text('展示内容:${pageData.value.toString()}'), ), ), ); } }
第二种思路:事件驱动刷新模式
对应你说的「读本地渲染→监听全局更新事件→事件触发后重读本地刷新」流程,适合多模块共享数据、其他模块负责更新数据的场景,和React里的EventEmitter订阅逻辑一致:
- 状态存储、初始读本地渲染的逻辑和第一种方案一致
- 页面初始化时注册全局事件监听器,页面销毁时注销监听器避免内存泄漏
- 其他模块完成API拉取、数据写入本地存储后,发送全局更新事件
- 当前页面收到事件后,重新读取本地存储的数据更新UI
首先实现一个最简全局事件总线,不需要引入额外依赖:
// 全局单例事件总线 class AppEventBus { AppEventBus._internal(); static final AppEventBus instance = AppEventBus._internal(); final _listenerMap = <String, List<void Function(dynamic)>>{}; // 订阅事件 void on(String eventName, void Function(dynamic) callback) { _listenerMap.putIfAbsent(eventName, () => []).add(callback); } // 发送事件 void emit(String eventName, [dynamic payload]) { if (_listenerMap[eventName] == null) return; for (final cb in List.from(_listenerMap[eventName]!)) { cb(payload); } } // 取消订阅 void off(String eventName, void Function(dynamic) callback) { _listenerMap[eventName]?.remove(callback); } } // 事件名常量 const kDataUpdatedEvent = 'app_data_updated';
页面实现代码:
class EventDrivenCachePage extends HookWidget { const EventDrivenCachePage({super.key}); @override Widget build(BuildContext context) { final pageData = useState<Map<String, dynamic>?>(null); // 抽离读本地缓存的公共逻辑 Future<void> loadFromLocal() async { final prefs = await SharedPreferences.getInstance(); final localCache = prefs.getString('page_cache_key'); if (localCache != null) { pageData.value = jsonDecode(localCache); } } useEffect(() { // 初始化先读本地数据 loadFromLocal(); // 注册数据更新事件监听 void eventHandler(dynamic _) { // 收到更新事件后重读本地数据刷新UI loadFromLocal(); } AppEventBus.instance.on(kDataUpdatedEvent, eventHandler); // 等价于React useEffect的清理函数,页面销毁时取消监听 return () { AppEventBus.instance.off(kDataUpdatedEvent, eventHandler); }; }, const []); return Scaffold( appBar: AppBar(title: const Text('事件驱动缓存页面')), body: Center( child: pageData.value == null ? const CircularProgressIndicator() : Padding( padding: const EdgeInsets.all(16), child: Text('展示内容:${pageData.value.toString()}'), ), ), ); } }
其他模块更新完数据并存入本地后,只需要调用AppEventBus.instance.emit(kDataUpdatedEvent),所有监听该事件的页面都会自动同步最新缓存,不需要跨页面传递数据。
补充说明
- 如果存储结构化模型数据,记得给实体类实现
toJson/fromJson序列化方法,不要直接把Dart对象实例存入本地存储 - 如果需要全局共享状态,可以把上述逻辑替换成Riverpod/Bloc等状态管理库的实现,核心流程不变
- 不建议用
FutureBuilder实现该逻辑的原因:FutureBuilder会等待传入的Future执行完成才会渲染子组件,如果把读本地和拉网络的逻辑放在同一个Future里,用户必须等网络请求完成才能看到内容,失去了本地缓存的意义
内容的提问来源于stack exchange,提问作者Mahdi Rafiei
相关产品推荐
相关产品推荐

