Ticker leak in TableColumnControlHandlesPopupRoute when disposing widget during column drag animation
Bug Description
When dragging columns in a table and the popup route is disposed while column translation animation is still running, a Ticker leak occurs. This results in the following error:
_WidgetState#b5d2f(tickers: tracking 2 tickers) was disposed with an active Ticker. _WidgetState created a Ticker via its TickerProviderStateMixin, but at the time dispose() was called on the mixin, that Ticker was still active. All Tickers must be disposed before calling super.dispose().
Steps to Reproduce
Create a table with draggable columns using TableColumnControlHandlesPopupRoute.realtime()
Start dragging a column to trigger the translation animation
While the animation is running, navigate away or dispose the widget (e.g., by scrolling the table)
The error occurs during widget disposal
Expected Behavior
The column translation Ticker should be properly stopped and disposed before the widget is disposed, preventing any Ticker leaks.
Actual Behavior
The Ticker created in _animateColumnTranslation() method remains active when the widget is disposed, causing a memory leak and the aforementioned error. Root Cause In _WidgetState class, the Ticker created in _animateColumnTranslation() method is not stored as a class field and therefore not properly disposed in the dispose() method. While there's proper handling for continuousScrollTicker, the column translation Ticker is missing similar cleanup.
Root Cause
In _WidgetState class, the Ticker created in _animateColumnTranslation() method is not stored as a class field and therefore not properly disposed in the dispose() method. While there's proper handling for continuousScrollTicker, the column translation Ticker is missing similar cleanup.
Environment
Flutter version: 3.x material_table_view version: 5.5.0 Platform: Windows
Yes, I've caught this during my testing but at the time I've came to the conclusion that the overhead of keeping track of a set of tickers was not worth it because there were little to no consequences to leaking one since everything gets disposed anyway. I'll revise this decision once I have a chance.
It seems I'm experiencing the same problem, but for me it prevents column moving from working. I couldn't yet isolate the problem, since the example app seems not to have that problem, and i'm not yet sure what I'm doing differently.. (Once the onColumnMove callback is called, i get the exceptions and the columns are stuck in place).
_WidgetState#cba94(tickers: tracking 1 ticker) was disposed with an active Ticker
======== Exception caught by widgets library =======================================================
The following assertion was thrown while finalizing the widget tree:
_WidgetState#cba94(tickers: tracking 1 ticker) was disposed with an active Ticker.
_WidgetState created a Ticker via its TickerProviderStateMixin, but at the time dispose() was called on the mixin, that Ticker was still active. All Tickers must be disposed before calling super.dispose().
Tickers used by AnimationControllers should be disposed by calling dispose() on the AnimationController itself. Otherwise, the ticker will leak.
The offending ticker was: _WidgetTicker(created by _WidgetState#cba94)
The stack trace when the _WidgetTicker was actually created was:
dart-sdk/lib/_internal/js_dev_runtime/patch/core_patch.dart 748:28 get current
package:flutter/src/scheduler/ticker.dart 86:40 <fn>
package:flutter/src/scheduler/ticker.dart 87:14 new
package:flutter/src/widgets/ticker_provider.dart 417:3 new
package:flutter/src/widgets/ticker_provider.dart 321:34 createTicker
package:material_table_view/src/table_column_control_handles_popup_route.dart 1389:14 [_animateColumnTranslation]
package:material_table_view/src/table_column_control_handles_popup_route.dart 1222:9 [_calculateMovement]
package:material_table_view/src/table_column_control_handles_popup_route.dart 1071:5 [_dragUpdate]
dart-sdk/lib/_internal/js_dev_runtime/private/ddc_runtime/operations.dart 117:77 tear
package:flutter/src/gestures/monodrag.dart 868:46 <fn>
package:flutter/src/gestures/recognizer.dart 345:24 invokeCallback
package:flutter/src/gestures/monodrag.dart 868:7 [_checkUpdate]
package:flutter/src/gestures/monodrag.dart 714:11 handleEvent
dart-sdk/lib/_internal/js_dev_runtime/private/ddc_runtime/operations.dart 117:77 tear
package:flutter/src/gestures/pointer_router.dart 97:7 [_dispatch]
package:flutter/src/gestures/pointer_router.dart 143:9 <fn>
dart-sdk/lib/_internal/js_dev_runtime/private/linked_hash_map.dart 21:7 forEach
package:flutter/src/gestures/pointer_router.dart 141:17 [_dispatchEventToRoutes]
package:flutter/src/gestures/pointer_router.dart 131:7 route
package:flutter/src/gestures/binding.dart 530:5 handleEvent
package:flutter/src/gestures/binding.dart 499:14 dispatchEvent
package:flutter/src/rendering/binding.dart 473:11 dispatchEvent
package:flutter/src/gestures/binding.dart 437:7 [_handlePointerEventImmediately]
package:flutter/src/gestures/binding.dart 394:5 handlePointerEvent
package:flutter/src/gestures/binding.dart 341:7 [_flushPointerEventQueue]
package:flutter/src/gestures/binding.dart 308:9 [_handlePointerDataPacket]
dart-sdk/lib/_internal/js_dev_runtime/private/ddc_runtime/operations.dart 117:77 tear
lib/_engine/engine/platform_dispatcher.dart 1327:5 invoke1
lib/_engine/engine/platform_dispatcher.dart 281:5 invokeOnPointerDataPacket
lib/_engine/engine/pointer_binding.dart 411:30 [_sendToFramework]
lib/_engine/engine/pointer_binding.dart 231:7 onPointerData
dart-sdk/lib/_internal/js_dev_runtime/private/ddc_runtime/operations.dart 117:77 tear
lib/_engine/engine/pointer_binding.dart 1002:16 <fn>
lib/_engine/engine/pointer_binding.dart 912:7 <fn>
lib/_engine/engine/pointer_binding.dart 535:9 loggedHandler
dart-sdk/lib/async/zone.dart 1849:54 runUnary
dart-sdk/lib/async/zone.dart 1804:26 <fn>
dart-sdk/lib/_internal/js_dev_runtime/patch/js_allow_interop_patch.dart 224:27 _callDartFunctionFast1
When the exception was thrown, this was the stack:
dart-sdk/lib/_internal/js_dev_runtime/private/ddc_runtime/errors.dart 266:3 throw_
package:flutter/src/widgets/ticker_provider.dart 371:13 <fn>
package:flutter/src/widgets/ticker_provider.dart 388:14 dispose
package:material_table_view/src/table_column_control_handles_popup_route.dart 603:11 dispose
package:flutter/src/widgets/framework.dart 5922:5 unmount
package:flutter/src/widgets/framework.dart 2075:12 [_unmount]
package:flutter/src/widgets/framework.dart 2073:7 <fn>
package:flutter/src/widgets/framework.dart 7118:9 visitChildren
package:flutter/src/widgets/framework.dart 2071:12 [_unmount]
package:flutter/src/widgets/framework.dart 2073:7 <fn>
package:flutter/src/widgets/framework.dart 5763:7 visitChildren
package:flutter/src/widgets/framework.dart 2071:12 [_unmount]
package:flutter/src/widgets/framework.dart 2073:7 <fn>
package:flutter/src/widgets/framework.dart 6994:7 visitChildren
package:flutter/src/widgets/framework.dart 2071:12 [_unmount]
package:flutter/src/widgets/framework.dart 2073:7 <fn>
package:flutter/src/widgets/framework.dart 6994:7 visitChildren
package:flutter/src/widgets/framework.dart 2071:12 [_unmount]
package:flutter/src/widgets/framework.dart 2073:7 <fn>
package:flutter/src/widgets/framework.dart 5763:7 visitChildren
package:flutter/src/widgets/framework.dart 2071:12 [_unmount]
package:flutter/src/widgets/framework.dart 2073:7 <fn>
package:flutter/src/widgets/framework.dart 5763:7 visitChildren
package:flutter/src/widgets/framework.dart 2071:12 [_unmount]
package:flutter/src/widgets/framework.dart 2073:7 <fn>
package:flutter/src/widgets/framework.dart 6994:7 visitChildren
package:flutter/src/widgets/framework.dart 2071:12 [_unmount]
package:flutter/src/widgets/framework.dart 2073:7 <fn>
package:flutter/src/widgets/framework.dart 5763:7 visitChildren
package:flutter/src/widgets/framework.dart 2071:12 [_unmount]
package:flutter/src/widgets/framework.dart 2073:7 <fn>
package:flutter/src/widgets/framework.dart 6994:7 visitChildren
package:flutter/src/widgets/framework.dart 2071:12 [_unmount]
package:flutter/src/widgets/framework.dart 2073:7 <fn>
package:flutter/src/widgets/framework.dart 5763:7 visitChildren
package:flutter/src/widgets/framework.dart 2071:12 [_unmount]
package:flutter/src/widgets/framework.dart 2073:7 <fn>
package:flutter/src/widgets/framework.dart 5763:7 visitChildren
package:flutter/src/widgets/framework.dart 2071:12 [_unmount]
package:flutter/src/widgets/framework.dart 2073:7 <fn>
package:flutter/src/widgets/framework.dart 5763:7 visitChildren
package:flutter/src/widgets/framework.dart 2071:12 [_unmount]
package:flutter/src/widgets/framework.dart 2073:7 <fn>
package:flutter/src/widgets/framework.dart 5763:7 visitChildren
package:flutter/src/widgets/framework.dart 2071:12 [_unmount]
package:flutter/src/widgets/framework.dart 2073:7 <fn>
package:flutter/src/widgets/framework.dart 5763:7 visitChildren
package:flutter/src/widgets/framework.dart 2071:12 [_unmount]
package:flutter/src/widgets/framework.dart 2073:7 <fn>
package:flutter/src/widgets/framework.dart 5763:7 visitChildren
package:flutter/src/widgets/framework.dart 2071:12 [_unmount]
package:flutter/src/widgets/framework.dart 2073:7 <fn>
package:flutter/src/widgets/framework.dart 6994:7 visitChildren
package:flutter/src/widgets/framework.dart 2071:12 [_unmount]
package:flutter/src/widgets/framework.dart 2073:7 <fn>
package:flutter/src/widgets/framework.dart 5763:7 visitChildren
package:flutter/src/widgets/framework.dart 2071:12 [_unmount]
package:flutter/src/widgets/framework.dart 2073:7 <fn>
package:flutter/src/widgets/framework.dart 5763:7 visitChildren
package:flutter/src/widgets/framework.dart 2071:12 [_unmount]
package:flutter/src/widgets/framework.dart 2073:7 <fn>
package:flutter/src/widgets/framework.dart 5763:7 visitChildren
package:flutter/src/widgets/framework.dart 2071:12 [_unmount]
package:flutter/src/widgets/framework.dart 2073:7 <fn>
package:flutter/src/widgets/framework.dart 5763:7 visitChildren
package:flutter/src/widgets/framework.dart 2071:12 [_unmount]
package:flutter/src/widgets/framework.dart 2073:7 <fn>
package:flutter/src/widgets/framework.dart 5763:7 visitChildren
package:flutter/src/widgets/framework.dart 2071:12 [_unmount]
package:flutter/src/widgets/framework.dart 2073:7 <fn>
package:flutter/src/widgets/framework.dart 6994:7 visitChildren
package:flutter/src/widgets/framework.dart 2071:12 [_unmount]
package:flutter/src/widgets/framework.dart 2073:7 <fn>
package:flutter/src/widgets/framework.dart 5763:7 visitChildren
package:flutter/src/widgets/framework.dart 2071:12 [_unmount]
package:flutter/src/widgets/framework.dart 2073:7 <fn>
package:flutter/src/widgets/framework.dart 5763:7 visitChildren
package:flutter/src/widgets/framework.dart 2071:12 [_unmount]
package:flutter/src/widgets/framework.dart 2073:7 <fn>
package:flutter/src/widgets/framework.dart 5763:7 visitChildren
package:flutter/src/widgets/framework.dart 2071:12 [_unmount]
package:flutter/src/widgets/framework.dart 2073:7 <fn>
package:flutter/src/widgets/framework.dart 5763:7 visitChildren
package:flutter/src/widgets/framework.dart 2071:12 [_unmount]
dart-sdk/lib/_internal/js_dev_runtime/private/ddc_runtime/operations.dart 117:77 tear
dart-sdk/lib/internal/iterable.dart 49:7 forEach
package:flutter/src/widgets/framework.dart 2084:15 [_unmountAll]
dart-sdk/lib/_internal/js_dev_runtime/private/ddc_runtime/operations.dart 117:77 tear
package:flutter/src/widgets/framework.dart 2965:7 lockState
package:flutter/src/widgets/framework.dart 3288:7 finalizeTree
package:flutter/src/widgets/binding.dart 1247:7 drawFrame
package:flutter/src/rendering/binding.dart 495:5 [_handlePersistentFrameCallback]
dart-sdk/lib/_internal/js_dev_runtime/private/ddc_runtime/operations.dart 117:77 tear
package:flutter/src/scheduler/binding.dart 1438:7 [_invokeFrameCallback]
package:flutter/src/scheduler/binding.dart 1351:9 handleDrawFrame
package:flutter/src/scheduler/binding.dart 1204:5 [_handleDrawFrame]
dart-sdk/lib/_internal/js_dev_runtime/private/ddc_runtime/operations.dart 117:77 tear
lib/_engine/engine/platform_dispatcher.dart 1312:5 invoke
lib/_engine/engine/platform_dispatcher.dart 255:5 invokeOnDrawFrame
lib/_engine/engine/frame_service.dart 189:32 [_renderFrame]
lib/_engine/engine/frame_service.dart 101:9 <fn>
dart-sdk/lib/async/zone.dart 1849:54 runUnary
dart-sdk/lib/async/zone.dart 1804:26 <fn>
dart-sdk/lib/_internal/js_dev_runtime/patch/js_allow_interop_patch.dart 224:27 _callDartFunctionFast1
====================================================================================================
followed by
DartError: Assertion failed: file:///Users/herbert/dev/flutter/flutter/packages/flutter/lib/src/widgets/framework.dart:5238:12 _lifecycleState != _ElementLifecycle.defunct is not true
======== Exception caught by scheduler library =====================================================
The following assertion was thrown during a scheduler callback:
Assertion failed: file:///Users/herbert/dev/flutter/flutter/packages/flutter/lib/src/widgets/framework.dart:5238:12
_lifecycleState != _ElementLifecycle.defunct
is not true
When the exception was thrown, this was the stack:
dart-sdk/lib/_internal/js_dev_runtime/private/ddc_runtime/errors.dart 266:3 throw_
dart-sdk/lib/_internal/js_dev_runtime/private/profile.dart 117:39 assertFailed
package:flutter/src/widgets/framework.dart 5238:49 markNeedsBuild
package:flutter/src/widgets/framework.dart 1219:5 setState
package:material_table_view/src/table_column_control_handles_popup_route.dart 659:22 <fn>
package:flutter/src/scheduler/binding.dart 1438:7 [_invokeFrameCallback]
package:flutter/src/scheduler/binding.dart 1365:11 handleDrawFrame
package:flutter/src/scheduler/binding.dart 1204:5 [_handleDrawFrame]
dart-sdk/lib/_internal/js_dev_runtime/private/ddc_runtime/operations.dart 117:77 tear
lib/_engine/engine/platform_dispatcher.dart 1312:5 invoke
lib/_engine/engine/platform_dispatcher.dart 255:5 invokeOnDrawFrame
lib/_engine/engine/frame_service.dart 189:32 [_renderFrame]
lib/_engine/engine/frame_service.dart 101:9 <fn>
dart-sdk/lib/async/zone.dart 1849:54 runUnary
dart-sdk/lib/async/zone.dart 1804:26 <fn>
dart-sdk/lib/_internal/js_dev_runtime/patch/js_allow_interop_patch.dart 224:27 _callDartFunctionFast1
====================================================================================================
which seems to throw in table_column_control_handles_popup_route.dart:659
void _parentDataChanged() =>
SchedulerBinding.instance.addPostFrameCallback((timeStamp) {
if (mounted) setState(() {});
});
Anyone else having this exception preventing column moving?
Anyone else having this exception preventing column moving?
Yes, I believe some people here have and this behavior is not caused by this issue. The route should not get disposed when the animation is running for it to work. Please check your state management to ensure the route does not get destoyed and recreated every time a column gets moved. Let me know if I've missed anything.
@NikolayNIK you are right, there is something wrong with my state management.. I'm using flutter_hooks and I was able to reproduce it.. i've changed (very crudely) the example app to use flutter_hooks and got the error when moving a column: https://github.com/NikolayNIK/material_table_view_demo/commit/d09ec34f922503c1759837cbcb402f1bc12259ce
the weird thing is, when I change the rebuild from the "state hook" back to setState, without changing anything else, it works again: https://github.com/NikolayNIK/material_table_view_demo/commit/53b889e80bfe0afb2f6b517ffa78bec421d972da ... I didn't expect that a change to a state hook would do anything else than a setState of a stateful widget.. but seems like I'm missing something 🙈
@NikolayNIK never mind.. the problem was creating a new instance of the list of columns.. when using TableView.builder(columns: columns.toList(), ...) moving columns doesn't work either.. imho this is a (separate) bug, arguments to widgets should always be immutable.. by passing a mutable list into the TableView it's pretty random that moving columns works in the first place.. imho everything should work with passing a new list instance into the widget on every build (or when the list changes) as well..