Flutter:Riverpod结合Firestore流更新的本地状态同步优化问题
Flutter Firestore + Riverpod 乐观更新的UI闪烁问题解决方案
问题背景
现有Flutter待办事项列表页面,具备两个核心功能:
- 点击列表项前置按钮设置唯一收藏项(同一时间仅一个项可标记为收藏)
- 通过
ReorderableDragStartListener实现拖拽排序
数据以Document形式存储在Firestore集合中,由基于AsyncNotifier的Riverpod Provider将文档转换为列表流提供给UI。
待办事项模型
class ToDoItem { ToDoItem({ required this.title, required this.order, required this.isFavorite }); final String title; final int order; final bool isFavorite; ToDoItem fromJson(Map<String, dynamic> json) => ...// 映射逻辑省略; }
Riverpod Provider(简化版)
Provider以流形式暴露待办列表,同时提供修改顺序和设置收藏的方法:
@riverpod class ToDoItemsController extends _$ToDoItemsController { @override Stream<List<ToDoItem>> build() { final _fs = ref.read(firestoreProvider); return _fs.collection('todo-items').orderBy('order').snapshots().map((snapshot) { return snapshot.docs.map((doc) { return ToDoItem.fromJson(doc.data()); }).toList(); }); } /// 修改项顺序 Future<void> changeOrder(ToDoItem item, int oldIndex, int newIndex) { // 乐观更新本地状态 final previousState = state.valueOrNull; if (previousState != null) { // ...本地顺序修改逻辑... } // 更新Firestore文档 _fs.collection('todo-items')...// 省略更新逻辑 } /// 设置收藏待办项 Future<void> setFavoriteToDoItem(ToDoItem item) { // 乐观更新本地状态 final newList = ...// 本地收藏状态修改逻辑... state = AsyncData(newList); // 更新Firestore文档 _fs.collection('todo-items')...// 省略更新逻辑 } }
核心问题
为实现UI即时响应,采用乐观更新:先修改本地状态,再同步Firestore。但由此引发UI异常:
- 收藏功能:UI会重建三次——本地乐观更新→旧收藏项Firestore更新触发流→新收藏项Firestore更新触发流,导致UI闪烁(状态来回切换)
- 排序功能:拖拽排序后项会来回跳变,原因是多次单个文档更新触发多次流事件,UI反复刷新
解决方案
1. 合并Firestore更新操作,减少流触发次数
Firestore的批量写操作是原子性的,一次提交多个文档更新只会触发一次流快照,从根源上减少UI重建次数。
收藏功能优化(批量写)
Future<void> setFavoriteToDoItem(ToDoItem item) async { final previousState = state.valueOrNull; if (previousState == null) return; // 1. 乐观更新本地状态:找到旧收藏项并取消,标记新项为收藏 final oldFavorite = previousState.firstWhere((e) => e.isFavorite, orElse: () => null); final newList = previousState.map((e) { if (e.title == item.title) return ToDoItem(title: e.title, order: e.order, isFavorite: true); if (oldFavorite != null && e.title == oldFavorite.title) return ToDoItem(title: e.title, order: e.order, isFavorite: false); return e; }).toList(); state = AsyncData(newList); // 2. Firestore批量写,一次性更新旧收藏项和新收藏项 final batch = ref.read(firestoreProvider).batch(); final collection = ref.read(firestoreProvider).collection('todo-items'); if (oldFavorite != null) { batch.update(collection.doc(oldFavorite.title), {'isFavorite': false}); } batch.update(collection.doc(item.title), {'isFavorite': true}); await batch.commit(); }
排序功能优化(批量写)
排序时通常需要调整多个项的order值,用批量写一次性提交所有变更:
Future<void> changeOrder(int oldIndex, int newIndex) async { final previousState = state.valueOrNull; if (previousState == null) return; // 1. 乐观更新本地状态 final reorderedList = List.of(previousState); final movedItem = reorderedList.removeAt(oldIndex); reorderedList.insert(newIndex, movedItem); // 重新计算所有项的order值 final updatedList = reorderedList.asMap().entries.map((entry) { return ToDoItem( title: entry.value.title, order: entry.key, isFavorite: entry.value.isFavorite, ); }).toList(); state = AsyncData(updatedList); // 2. Firestore批量写更新所有受影响的项 final batch = ref.read(firestoreProvider).batch(); final collection = ref.read(firestoreProvider).collection('todo-items'); for (final item in updatedList) { batch.update(collection.doc(item.title), {'order': item.order}); } await batch.commit(); }
2. 本地状态与流快照的去重处理
即使使用批量写,仍可能存在本地状态与流快照的微小差异,可以在Provider的流转换中添加去重逻辑,避免不必要的UI重建:
@override Stream<List<ToDoItem>> build() { final _fs = ref.read(firestoreProvider); return _fs.collection('todo-items').orderBy('order').snapshots() .map((snapshot) { return snapshot.docs.map((doc) => ToDoItem.fromJson(doc.data())).toList(); }) // 只在列表内容真正变化时才触发更新 .distinct((prev, next) { if (prev.length != next.length) return false; for (int i = 0; i < prev.length; i++) { final p = prev[i]; final n = next[i]; if (p.title != n.title || p.order != n.order || p.isFavorite != n.isFavorite) { return false; } } return true; }); }
3. 乐观更新与Firestore操作的一致性
确保本地乐观更新的逻辑和Firestore的更新逻辑完全一致,这样流返回的快照会和本地状态完全匹配,UI不会出现额外的状态切换。比如收藏功能中,本地同时修改新旧项的状态,和批量写的操作完全对应;排序功能中本地重新计算所有项的order值,和批量写的更新逻辑一致。
内容的提问来源于stack exchange,提问作者Mike
相关产品推荐
相关产品推荐

