You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.30 14:30:00