Skip to content

Commit 06ca902

Browse files
authored
Slider should check mounted before start interaction (flutter#132010)
This is a follow up to the following pull requests: - flutter#124514 I was finally able to reproduce this bug and found out why it was happening. Consider this code: ```dart GestureDetector( behavior: HitTestBehavior.translucent, // Note: Make sure onTap is not null to ensure events // are captured by `GestureDetector` onTap: () {}, child: _shouldShowSlider ? Slider(value: _value, onChanged: _handleSlide) : const SizedBox.shrink(). ) ``` Runtime exception happens when: 1. User taps and holds the Slider (drag callback captured by `GestureDetector`) 2. `_shouldShowSlider` changes to false, Slider disappears and unmounts, and unregisters `_handleSlide`. But the callback is still registered by `GestureDetector` 3. Users moves finger as if Slider were still there 4. Drag callback is invoked, `_SliderState.showValueIndicator` is called 5. Exception - Slider is already disposed This pull request fixes it by adding a mounted check inside `_SliderState.showValueIndicator` to ensure the Slider is actually mounted at the time of invoking drag event callback. I've added a unit test that will fail without this change. The error stack trace is: ``` The following assertion was thrown while handling a gesture: This widget has been unmounted, so the State no longer has a context (and should be considered defunct). Consider canceling any active work during "dispose" or using the "mounted" getter to determine if the State is still active. When the exception was thrown, this was the stack: #0 State.context.<anonymous closure> (package:flutter/src/widgets/framework.dart:950:9) #1 State.context (package:flutter/src/widgets/framework.dart:956:6) #2 _SliderState.showValueIndicator (package:flutter/src/material/slider.dart:968:18) #3 _RenderSlider._startInteraction (package:flutter/src/material/slider.dart:1487:12) #4 _RenderSlider._handleDragStart (package:flutter/src/material/slider.dart:1541:5) #5 DragGestureRecognizer._checkStart.<anonymous closure> (package:flutter/src/gestures/monodrag.dart:531:53) #6 GestureRecognizer.invokeCallback (package:flutter/src/gestures/recognizer.dart:275:24) #7 DragGestureRecognizer._checkStart (package:flutter/src/gestures/monodrag.dart:531:7) #8 DragGestureRecognizer._checkDrag (package:flutter/src/gestures/monodrag.dart:498:5) #9 DragGestureRecognizer.acceptGesture (package:flutter/src/gestures/monodrag.dart:431:7) #10 _CombiningGestureArenaMember.acceptGesture (package:flutter/src/gestures/team.dart:45:14) #11 GestureArenaManager._resolveInFavorOf (package:flutter/src/gestures/arena.dart:281:12) #12 GestureArenaManager._resolve (package:flutter/src/gestures/arena.dart:239:9) #13 GestureArenaEntry.resolve (package:flutter/src/gestures/arena.dart:53:12) #14 _CombiningGestureArenaMember._resolve (package:flutter/src/gestures/team.dart:85:15) #15 _CombiningGestureArenaEntry.resolve (package:flutter/src/gestures/team.dart:19:15) #16 OneSequenceGestureRecognizer.resolve (package:flutter/src/gestures/recognizer.dart:375:13) #17 DragGestureRecognizer.handleEvent (package:flutter/src/gestures/monodrag.dart:414:13) #18 PointerRouter._dispatch (package:flutter/src/gestures/pointer_router.dart:98:12) #19 PointerRouter._dispatchEventToRoutes.<anonymous closure> (package:flutter/src/gestures/pointer_router.dart:143:9) #20 _LinkedHashMapMixin.forEach (dart:collection-patch/compact_hash.dart:625:13) #21 PointerRouter._dispatchEventToRoutes (package:flutter/src/gestures/pointer_router.dart:141:18) #22 PointerRouter.route (package:flutter/src/gestures/pointer_router.dart:127:7) #23 GestureBinding.handleEvent (package:flutter/src/gestures/binding.dart:488:19) #24 GestureBinding.dispatchEvent (package:flutter/src/gestures/binding.dart:468:22) #25 RendererBinding.dispatchEvent (package:flutter/src/rendering/binding.dart:439:11) #26 GestureBinding._handlePointerEventImmediately (package:flutter/src/gestures/binding.dart:413:7) #27 GestureBinding.handlePointerEvent (package:flutter/src/gestures/binding.dart:376:5) #28 GestureBinding._flushPointerEventQueue (package:flutter/src/gestures/binding.dart:323:7) #29 GestureBinding._handlePointerDataPacket (package:flutter/src/gestures/binding.dart:292:9) #30 _invoke1 (dart:ui/hooks.dart:186:13) #31 PlatformDispatcher._dispatchPointerDataPacket (dart:ui/platform_dispatcher.dart:433:7) #32 _dispatchPointerDataPacket (dart:ui/hooks.dart:119:31) Handler: "onStart" Recognizer: HorizontalDragGestureRecognizer#a5df2 ``` *List which issues are fixed by this PR. You must list at least one issue.* Internal bug: b/273666179, b/192329942 *If you had to change anything in the [flutter/tests] repo, include a link to the migration guide as per the [breaking change policy].*
1 parent 122c429 commit 06ca902

File tree

2 files changed

+27
-19
lines changed

2 files changed

+27
-19
lines changed

packages/flutter/lib/src/material/slider.dart

+3
Original file line numberDiff line numberDiff line change
@@ -1484,6 +1484,9 @@ class _RenderSlider extends RenderBox with RelayoutWhenSystemFontsChangeMixin {
14841484
}
14851485

14861486
void _startInteraction(Offset globalPosition) {
1487+
if (!_state.mounted) {
1488+
return;
1489+
}
14871490
_state.showValueIndicator();
14881491
if (!_active && isInteractive) {
14891492
switch (allowedInteraction) {

packages/flutter/test/material/slider_test.dart

+24-19
Original file line numberDiff line numberDiff line change
@@ -3654,16 +3654,22 @@ void main() {
36543654
child: ValueListenableBuilder<bool>(
36553655
valueListenable: shouldShowSliderListenable,
36563656
builder: (BuildContext context, bool shouldShowSlider, _) {
3657-
return shouldShowSlider
3658-
? Slider(
3659-
value: value,
3660-
onChanged: (double newValue) {
3661-
setState(() {
3662-
value = newValue;
3663-
});
3664-
},
3665-
)
3666-
: const SizedBox.shrink();
3657+
return GestureDetector(
3658+
behavior: HitTestBehavior.translucent,
3659+
// Note: it is important that `onTap` is non-null so
3660+
// [GestureDetector] will register tap events.
3661+
onTap: () {},
3662+
child: shouldShowSlider
3663+
? Slider(
3664+
value: value,
3665+
onChanged: (double newValue) {
3666+
setState(() {
3667+
value = newValue;
3668+
});
3669+
},
3670+
)
3671+
: const SizedBox.expand(),
3672+
);
36673673
},
36683674
),
36693675
),
@@ -3674,20 +3680,19 @@ void main() {
36743680
),
36753681
);
36763682

3683+
// Move Slider.
36773684
final TestGesture gesture = await tester
3678-
.startGesture(tester.getRect(find.byType(Slider)).centerLeft);
3685+
.startGesture(tester.getRect(find.byType(Slider)).center);
3686+
await gesture.moveBy(const Offset(1.0, 0.0));
3687+
await tester.pumpAndSettle();
36793688

3680-
// Intentionally not calling `await tester.pumpAndSettle()` to allow drag
3681-
// event performed on `Slider` before it is about to get unmounted.
3689+
// Hide Slider. Slider will dispose and unmount.
36823690
shouldShowSliderListenable.value = false;
3683-
3684-
await tester.drag(find.byType(Slider), const Offset(1.0, 0.0));
36853691
await tester.pumpAndSettle();
36863692

3687-
expect(value, equals(0.0));
3688-
3689-
// This is supposed to trigger animation on `Slider` if it is mounted.
3690-
await gesture.up();
3693+
// Move Slider after unmounted.
3694+
await gesture.moveBy(const Offset(1.0, 0.0));
3695+
await tester.pumpAndSettle();
36913696

36923697
expect(tester.takeException(), null);
36933698
});

0 commit comments

Comments
 (0)