Flutter two_dimensional_scrollables鼠标滚轮快速滚动崩溃求助
我最近在Flutter项目中切换到two_dimensional_scrollables组件,替换了原来的双ScrollView方案,该组件的懒加载特性带来了更好的性能。我在TableView.builder()外层使用了带有CustomScrollBehaviour的ScrollConfiguration,以支持鼠标拖拽滚动。为充分利用懒加载,我将TableView拆分为多个小块。
现在遇到的问题:当使用鼠标滚轮快速滚动(或滚动速度稍慢但同时显示更多元素)时,组件会崩溃并逐渐消失,即使热重启也无法恢复,必须完全重启应用。拖拽滚动(包括鼠标拖拽)时无此问题。
已尝试的操作
- 排查发现问题在低算力设备上更明显,当TableView的小块需要计算构建时问题更严重
- 通过缓存已构建的组件,问题有所缓解但未解决
环境信息
- 问题出现平台:Web、Windows、Linux;Android因使用触摸滚动暂未出现该问题
- 当前使用版本:two_dimensional_scrollables 0.2.1 + Flutter 3.19.5
错误信息
The following assertion was thrown during performLayout(): 'package:flutter/src/widgets/two_dimensional_viewport.dart': Failed assertion: line 783 pos 12: '_children.containsValue(child)': is not true. The relevant error-causing widget was: TableView TableView:file:///home/<etc>/<etc>/file.dart:1131:40 When the exception was thrown, this was the stack: #2 RenderTwoDimensionalViewport.parentDataOf (package:flutter/src/widgets/two_dimensional_viewport.dart:783:12) #3 RenderTableViewport.parentDataOf (package:two_dimensional_scrollables/src/table_view/table.dart:395:13) #4 RenderTwoDimensionalViewport.performLayout.<anonymous closure>.<anonymous closure> (package:flutter/src/widgets/two_dimensional_viewport.dart:1264:17) #5 WhereIterator.moveNext (dart:_internal/iterable.dart:451:13) #6 Iterable.isEmpty (dart:core/iterable.dart:540:33) #7 RenderTwoDimensionalViewport.performLayout.<anonymous closure> (package:flutter/src/widgets/two_dimensional_viewport.dart:1265:10) #8 RenderObject.invokeLayoutCallback.<anonymous closure> (package:flutter/src/rendering/object.dart:2686:59) #9 PipelineOwner._enableMutationsToDirtySubtrees (package:flutter/src/rendering/object.dart:1097:15) #10 RenderObject.invokeLayoutCallback (package:flutter/src/rendering/object.dart:2686:14) #11 RenderTwoDimensionalViewport.performLayout (package:flutter/src/widgets/two_dimensional_viewport.dart:1258:5) #12 RenderObject._layoutWithoutResize (package:flutter/src/rendering/object.dart:2414:7) #13 PipelineOwner.flushLayout (package:flutter/src/rendering/object.dart:1051:18) #14 PipelineOwner.flushLayout (package:flutter/src/rendering/object.dart:1064:15) #15 RendererBinding.drawFrame (package:flutter/src/rendering/binding.dart:582:23) #16 WidgetsBinding.drawFrame (package:flutter/src/widgets/binding.dart:991:13) #17 RendererBinding._handlePersistentFrameCallback (package:flutter/src/rendering/binding.dart:448:5) #18 SchedulerBinding._invokeFrameCallback (package:flutter/src/scheduler/binding.dart:1386:15) #19 SchedulerBinding.handleDrawFrame (package:flutter/src/scheduler/binding.dart:1311:9) #20 SchedulerBinding._handleDrawFrame (package:flutter/src/scheduler/binding.dart:1169:5) #21 _invoke (dart:ui/hooks.dart:312:13) #22 PlatformDispatcher._drawFrame (dart:ui/platform_dispatcher.dart:399:5) #23 _drawFrame (dart:ui/hooks.dart:283:31) (elided 2 frames from class _AssertionError) The following RenderObject was being processed when the exception was fired: RenderTableViewport#a1771 NEEDS-LAYOUT NEEDS-COMPOSITING-BITS-UPDATE ... needs compositing ... parentData: <none> (can use size) ... constraints: BoxConstraints(w=2133.3, h=919.2) ... layer: OffsetLayer#1f62a ... engine layer: OffsetEngineLayer#b03e9 ... handles: 2 ... offset: Offset(213.3, 356.9) ... size: Size(2133.3, 919.2) RenderObject: RenderTableViewport#a1771 NEEDS-LAYOUT NEEDS-COMPOSITING-BITS-UPDATE needs compositing parentData: <none> (can use size) constraints: BoxConstraints(w=2133.3, h=919.2) layer: OffsetLayer#1f62a engine layer: OffsetEngineLayer#b03e9 handles: 2 offset: Offset(213.3, 356.9) size: Size(2133.3, 919.2) ... (row: 0, column: 0): RenderRepaintBoundary#b39ec ... needs compositing ... parentData: vicinity=(row: 0, column: 0); layoutOffset=Offset(0.0, 0.0); paintOffset=Offset(0.0, 0.0); visible - paintExtent=Size(610.0, 150.0); ... constraints: BoxConstraints(w=610.0, h=150.0) ... layer: OffsetLayer#f7b0b ... engine layer: OffsetEngineLayer#89d12 ... handles: 2 ... offset: Offset(0.0, 0.0) ... size: Size(610.0, 150.0) ... metrics: 97.0% useful (1 bad vs 32 good) ... diagnosis: this is an outstandingly useful repaint boundary and should definitely be kept ... child: RenderFlex#80fea ... parentData: <none> (can use size) ... constraints: BoxConstraints(w=610.0, h=150.0) ... size: Size(610.0, 150.0) ... direction: horizontal ... mainAxisAlignment: start ... mainAxisSize: max ... crossAxisAlignment: center ... textDirection: ltr ... verticalDirection: down ... child 1: RenderSemanticsAnnotations#94584 relayoutBoundary=up1 ... parentData: offset=Offset(0.0, 4.0); flex=null; fit=null (can use size) ... constraints: BoxConstraints(0.0<=w<=Infinity, 0.0<=h<=150.0) ... semantics node: SemanticsNode#148 ... Rect.fromLTRB(0.0, 4.0, 306.0, 146.0) ... label: "Gesamt ======== Exception caught by rendering library ===================================================== The following assertion was thrown during performLayout(): 'package:flutter/src/widgets/two_dimensional_viewport.dart': Failed assertion: line 783 pos 12: '_children.containsValue(child)': is not true. The relevant error-causing widget was: TableView TableView:file:///home/<etc>/<etc>/file.dart:1131:40 When the exception was thrown, this was the stack: #2 RenderTwoDimensionalViewport.parentDataOf (package:flutter/src/widgets/two_dimensional_viewport.dart:783:12) #3 RenderTableViewport.parentDataOf (package:two_dimensional_scrollables/src/table_view/table.dart:395:13) #4 RenderTwoDimensionalViewport.performLayout.<anonymous closure>.<anonymous closure> (package:flutter/src/widgets/two_dimensional_viewport.dart:1264:17) #5 WhereIterator.moveNext (dart:_internal/iterable.dart:451:13) #6 Iterable.isEmpty (dart:core/iterable.dart:540:33) #7 RenderTwoDimensionalViewport.performLayout.<anonymous closure> (package:flutter/src/widgets/two_dimensional_viewport.dart:1265:10) #8 RenderObject.invokeLayoutCallback.<anonymous closure> (package:flutter/src/rendering/object.dart:2686:59) #9 PipelineOwner._enableMutationsToDirtySubtrees (package:flutter/src/rendering/object.dart:1097:15) #10 RenderObject.invokeLayoutCallback (package:flutter/src/rendering/object.dart:2686:14) #11 RenderTwoDimensionalViewport.performLayout (package:flutter/src/widgets/two_dimensional_viewport.dart:1258:5) #12 RenderObject._layoutWithoutResize (package:flutter/src/rendering/object.dart:2414:7) #13 PipelineOwner.flushLayout (package:flutter/src/rendering/object.dart:1051:18) #14 PipelineOwner.flushLayout (package:flutter/src/rendering/object.dart:1064:15) #15 RendererBinding.drawFrame (package:flutter/src/rendering/binding.dart:582:23) #16 WidgetsBinding.drawFrame (package:flutter/src/widgets/binding.dart:991:13) #17 RendererBinding._handlePersistentFrameCallback (package:flutter/src/rendering/binding.dart:448:5) #18 SchedulerBinding._invokeFrameCallback (package:flutter/src/scheduler/binding.dart:1386:15) #19 SchedulerBinding.handleDrawFrame (package:flutter/src/scheduler/binding.dart:1311:9) #20 SchedulerBinding._handleDrawFrame (package:flutter/src/scheduler/binding.dart:1169:5) #21 _invoke (dart:ui/hooks.dart:312:13) #22 PlatformDispatcher._drawFrame (dart:ui/platform_dispatcher.dart:399:5) #23 _drawFrame (dart:ui/hooks.dart:283:31) (elided 2 frames from class _AssertionError) The following RenderObject was being processed when the exception was fired: RenderTableViewport#a1771 NEEDS-LAYOUT NEEDS-COMPOSITING-BITS-UPDATE ... needs compositing ... parentData: <none> (can use size) ... constraints: BoxConstraints(w=2133.3, h=919.2) ... layer: OffsetLayer#1f62a ... engine layer: OffsetEngineLayer#278ca ... handles: 2 ... offset: Offset(213.3, 356.9) ... size: Size(2133.3, 919.2) RenderObject: RenderTableViewport#a1771 NEEDS-LAYOUT NEEDS-COMPOSITING-BITS-UPDATE needs compositing parentData: <none> (can use size) constraints: BoxConstraints(w=2133.3, h=919.2) layer: OffsetLayer#1f62a engine layer: OffsetEngineLayer#278ca handles: 2 offset: Offset(213.3, 356.9) size: Size(2133.3, 919.2) ... (row: 0, column: 0): RenderRepaintBoundary#b39ec ... needs compositing ... parentData: vicinity=(row: 0, column: 0); layoutOffset=Offset(0.0, 0.0); paintOffset=Offset(0.0, 0.0); visible - paintExtent=Size(610.0, 150.0); ... constraints: BoxConstraints(w=610.0, h=150.0) ... layer: OffsetLayer#f7b0b ... engine layer: OffsetEngineLayer#89d12 ... handles: 2 ... offset: Offset(0.0, 0.0) ... size: Size(610.0, 150.0) ... metrics: 97.1% useful (1 bad vs 33 good) ... diagnosis: this is an outstandingly useful repaint boundary and should definitely be kept ... child: RenderFlex#80fea ... parentData: <none> (can use size) ... constraints: BoxConstraints(w=610.0, h=150.0) ... size: Size(610.0, 150.0) ... direction: horizontal ... mainAxisAlignment: start ... mainAxisSize: max ... crossAxisAlignment: center ... textDirection: ltr ... verticalDirection: down ... child 1: RenderSemanticsAnnotations#94584 relayoutBoundary=up1 ... parentData: offset=Offset(0.0, 4.0); flex=null; fit=null (can use size) ... constraints: BoxConstraints(0.0<=w<=Infinity, 0.0<=h<=150.0) ... semantics node: SemanticsNode#148 ... STALE ... owner: null ... Rect.fromLTRB(0.0, 4.0, 306.0, 146.0) ... label: "Gesamt Gesamt Gesamt Projekte: 59 Projekte: 59
这个断言错误源于two_dimensional_viewport布局时尝试访问已被移除的子元素,属于组件内部状态不同步问题,结合你的场景可尝试以下方案:
升级依赖版本
当前使用的two_dimensional_scrollables 0.2.1版本较旧,后续版本大概率修复了这类布局同步问题。直接升级到最新稳定版,同时确保Flutter版本兼容(建议升级到3.22及以上)。调整滚动行为逻辑
自定义的CustomScrollBehaviour可能与组件滚动事件处理冲突,尤其是鼠标滚轮快速滚动触发的频繁布局更新。可尝试:- 简化
CustomScrollBehaviour,仅保留鼠标拖拽支持,移除多余事件拦截逻辑 - 给鼠标滚轮添加滚动阻尼,减少单次滚动触发的布局更新次数,示例代码:
@override ScrollPhysics getScrollPhysics(BuildContext context) { return const ClampingScrollPhysics(parent: BouncingScrollPhysics()) .applyTo(const ScrollPhysics()); }
- 简化
优化TableView分块策略
过小的分块会导致滚动时频繁触发组件创建与销毁,加剧状态不同步。尝试调大分块尺寸,减少分块数量,降低布局计算压力。固定TableView状态
给TableView添加全局GlobalKey,确保组件在热重载或布局更新时不丢失状态,减少状态重置导致的子元素异常:final tableViewKey = GlobalKey(); // 在build中使用 TableView.builder( key: tableViewKey, // ...其他参数 )排查语义节点问题
错误日志中出现STALE语义节点,可能是语义更新与布局更新不同步导致的。可尝试在TableView子组件中禁用语义注解,或手动控制语义节点更新时机。
若以上方案均无效,建议在Flutter官方仓库提交issue,附上复现代码与环境信息,协助官方定位修复。
内容的提问来源于stack exchange,提问作者yahoooza

