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

如何解决StreamBuilder中ReorderableListView的拖拽延迟问题?

问题

我有一个按特定顺序展示行程列表的页面,此前通过StreamBuilder内的ListView监听本地Drift数据库的有序查询结果进行展示。使用StreamBuilder是因为每个行程可编辑,希望行程更新时列表能实时显示最新变化。但将ListView改为ReorderableListView后,通过数据库操作更新行程顺序时,会出现轻微延迟:列表项先跳回原位置,之后才正确更新,体验不佳。我了解直接通过数据库而非状态更新并非最优方案,想请教最佳解决方法,既能实现行程编辑时UI响应式更新,又能避免拖拽时操作数据库(仅在保存时将更新后的顺序同步到数据库),Provider包是否能起到帮助?

当前onReorder函数(已知存在索引问题,后续会修复):

onReorder: (oldIndex, newIndex) {
    // 需要将rank+1的项
    final incrementRanks = [
        for (var i = min(oldIndex, newIndex) + 1;
            i <= max(oldIndex, newIndex) - 1;
            i++)
            i
    ];

    // 被移动项的ID
    final movedId = snapshot.data![oldIndex].id;

    // 需要更新rank的项的ID列表
    final incrementIdList = snapshot.data!
        .sublist(min(oldIndex, newIndex) + 1,
            max(oldIndex, newIndex))
        .map((stage) => stage.id)
        .toList();

    tourService.updateStageRank(newIndex, movedId!);
    incrementRanks.asMap().forEach((index, rank) {
        tourService.updateStageRank(
            rank + 1, incrementIdList[index]!);
    });
}

编辑补充:
StreamBuilder中的流来自以下函数:

Stream<List<Stage>> getStagesStream(String tourId) {
    return localDatabase.tourDao.getStages(tourId).watch();
}

该流监听本地Drift数据库中的如下SQL查询:

getStages: SELECT * FROM Stages
           WHERE tour_id = :tour_id
           ORDER BY rank;
解决方案

Provider完全可以解决这个问题,核心思路是用Provider管理内存中的行程列表状态:拖拽时只更新内存状态(保证UI即时响应),编辑时同步更新内存和数据库(保证实时性),用户确认保存时再把内存中的顺序同步到数据库。

步骤1:创建Provider状态类

定义一个TourStagesProvider,统一管理行程列表的内存状态与数据库交互:

class TourStagesProvider extends ChangeNotifier {
  final TourService tourService;
  final String tourId;
  late List<Stage> _stages;

  List<Stage> get stages => _stages;

  TourStagesProvider({required this.tourService, required this.tourId}) {
    _initStages();
  }

  // 从数据库加载初始数据并监听后续变化
  Future<void> _initStages() async {
    _stages = await tourService.getStagesStream(tourId).first;
    notifyListeners();
    // 监听数据库编辑操作,同步内存状态
    tourService.getStagesStream(tourId).listen((newStages) {
      _stages = newStages;
      notifyListeners();
    });
  }

  // 拖拽时仅更新内存中的顺序
  void reorderStages(int oldIndex, int newIndex) {
    if (newIndex > oldIndex) newIndex--;
    final movedStage = _stages.removeAt(oldIndex);
    _stages.insert(newIndex, movedStage);
    notifyListeners();
  }

  // 保存时批量将内存顺序同步到数据库
  Future<void> saveStageOrder() async {
    for (int i = 0; i < _stages.length; i++) {
      await tourService.updateStageRank(i, _stages[i].id!);
    }
  }

  // 处理行程编辑:同步更新内存与数据库
  Future<void> updateStage(Stage updatedStage) async {
    await tourService.updateStage(updatedStage);
    // 直接更新内存,无需等待数据库流触发
    final index = _stages.indexWhere((s) => s.id == updatedStage.id);
    if (index != -1) {
      _stages[index] = updatedStage;
      notifyListeners();
    }
  }
}

步骤2:重构页面使用Provider与ReorderableListView

替换原StreamBuilder,用Consumer监听Provider状态变化,拖拽时仅调用内存状态更新方法:

class TourStagesPage extends StatelessWidget {
  final String tourId;

  const TourStagesPage({super.key, required this.tourId});

  @override
  Widget build(BuildContext context) {
    return ChangeNotifierProvider(
      create: (context) => TourStagesProvider(
        tourService: context.read<TourService>(),
        tourId: tourId,
      ),
      child: Scaffold(
        appBar: AppBar(
          title: const Text("行程列表"),
          actions: [
            TextButton(
              onPressed: () {
                context.read<TourStagesProvider>().saveStageOrder();
                ScaffoldMessenger.of(context).showSnackBar(
                  const SnackBar(content: Text("顺序已保存")),
                );
              },
              child: const Text("保存"),
            ),
          ],
        ),
        body: Consumer<TourStagesProvider>(
          builder: (context, provider, child) {
            return ReorderableListView(
              onReorder: provider.reorderStages,
              children: provider.stages.map((stage) {
                return ListTile(
                  key: Key(stage.id.toString()),
                  title: Text(stage.name),
                  subtitle: Text(stage.description),
                  onTap: () {
                    Navigator.push(
                      context,
                      MaterialPageRoute(
                        builder: (context) => StageEditPage(
                          stage: stage,
                          onSave: (updated) {
                            provider.updateStage(updated);
                            Navigator.pop(context);
                          },
                        ),
                      ),
                    );
                  },
                );
              }).toList(),
            );
          },
        ),
      ),
    );
  }
}

步骤3:优化核心逻辑

  1. 拖拽无延迟:拖拽操作仅修改内存列表,UI即时响应,不会出现跳回原位置的问题
  2. 编辑实时同步:编辑行程时同时更新内存与数据库,保证UI立即刷新
  3. 数据库操作优化:仅在用户点击保存时批量更新数据库,减少IO次数与流触发频率
  4. 状态统一:所有行程状态由Provider管理,避免StreamBuilder与本地状态不一致的问题

内容的提问来源于stack exchange,提问作者MKJ

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.17 06:55:21