Flutter开发:页面切换重复读DB/SharedPreferences,该每次查询还是用单例存值?
单例可以用来做缓存,但它只是实现数据缓存的其中一种方式,不是万能解,也不是非用不可。
先搞清楚为啥会反复查库
这类重复查询的本质,是两类代码写出来的问题:
- 把DB查询逻辑写在了
build方法里:只要页面触发任意 rebuild(比如弹个提示框、切个主题、父组件状态更新)就会重新走查库流程,根本不是切页才触发 - 页面被销毁重建时,没有做前置缓存判断:每次进入页面(包括从其他页面切回)都从头走一遍initState里的查库逻辑,没有判断之前是不是已经加载过同一份数据
到底要不要写单例数据类当缓存?
要分场景,别上来就写个全局单例把所有DB返回的数据全塞进去,最后变成全局垃圾变量库:
- 适合用单例缓存的场景:多个页面都要用到的同一份公共业务数据,比如用户基础信息、全局配置、同一条业务流里多页共享的列表数据
- 不适合用单例缓存的场景:只有单个页面会用到的临时数据、需要随页面操作频繁变化的瞬时状态(比如页面内的输入草稿、弹窗的临时选择值),塞单例里反而容易出现脏数据bug,排查半天找不到问题根源
不同场景下的低成本实现方案
- 单页专属数据:根本不用写单例,页面级缓存就够
- 直接在对应的页面State里存一份已加载的数据,加个布尔标记判断是不是已经加载完成,只有标记为未加载时才去查DB,下次切回页面只要没主动清空标记,就直接用内存里存的数据渲染,等用户主动下拉刷新时再重置标记重新查库就行
- 如果你是用
IndexedStack实现的底部Tab切页,本身页面State不会被销毁,只要别把查库逻辑写在build里,天然就不会出现重复查询的问题
- 多页共享数据:写轻量Repository层当单例缓存,别搞大杂烩
- 单例不要直接持有DB原始对象,做一层和业务域绑定的Repository,每次读数据先判断内存缓存里有没有结果,有就直接返回,没有再去查DB/SharedPreferences,查完把结果存在内存缓存里
- 必须给缓存加配套的失效/更新逻辑:比如用户退出登录时清空缓存,业务数据被修改时主动同步更新缓存值,别让缓存一直存着旧数据
- 简单实现参考:
class OrderRepository { OrderRepository._(); static final OrderRepository instance = OrderRepository._(); List<Order>? _cachedOrderList; Future<List<Order>> getOrderList() async { // 有缓存直接返回,不碰DB if (_cachedOrderList != null) return _cachedOrderList!; // 无缓存再查询 final res = await DB.queryUserOrders(); _cachedOrderList = res; return res; } // 新增订单后主动刷新缓存,避免旧数据 Future<void> addNewOrder(Order order) async { await DB.insertOrder(order); _cachedOrderList?.insert(0, order); } // 退出登录时清空所有缓存 void clearAllCache() { _cachedOrderList = null; } }
- 已经在用状态管理框架的话,别额外写单例
- 不管你用的是Provider、Riverpod还是Bloc,框架提供的全局Store本身就是天然的缓存层,直接把需要共享的业务数据存在Store里就行,没必要重复造单例的轮子
要避开的常见反模式
- 永远别在
build方法里写DB/SharedPreferences的读写逻辑,build的执行频率远高于你的预期,会造成大量无意义的IO操作 - 单例里别持有
BuildContext、页面State这类和页面生命周期绑定的对象,否则必然会内存泄漏 - 别让缓存永久常驻:长时间没用到的缓存要及时清理,别把单例变成内存溢出的重灾区
内容的提问来源于stack exchange,提问作者stacktrace2234
相关产品推荐
相关产品推荐

