Skip to content

Commit

Permalink
Slider should check mounted before start interaction (flutter#132010)
Browse files Browse the repository at this point in the history
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].*
  • Loading branch information
liumcse committed Aug 7, 2023
1 parent 122c429 commit 06ca902
Show file tree
Hide file tree
Showing 2 changed files with 27 additions and 19 deletions.
3 changes: 3 additions & 0 deletions packages/flutter/lib/src/material/slider.dart
Original file line number Diff line number Diff line change
Expand Up @@ -1484,6 +1484,9 @@ class _RenderSlider extends RenderBox with RelayoutWhenSystemFontsChangeMixin {
}

void _startInteraction(Offset globalPosition) {
if (!_state.mounted) {
return;
}
_state.showValueIndicator();
if (!_active && isInteractive) {
switch (allowedInteraction) {
Expand Down
43 changes: 24 additions & 19 deletions packages/flutter/test/material/slider_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3654,16 +3654,22 @@ void main() {
child: ValueListenableBuilder<bool>(
valueListenable: shouldShowSliderListenable,
builder: (BuildContext context, bool shouldShowSlider, _) {
return shouldShowSlider
? Slider(
value: value,
onChanged: (double newValue) {
setState(() {
value = newValue;
});
},
)
: const SizedBox.shrink();
return GestureDetector(
behavior: HitTestBehavior.translucent,
// Note: it is important that `onTap` is non-null so
// [GestureDetector] will register tap events.
onTap: () {},
child: shouldShowSlider
? Slider(
value: value,
onChanged: (double newValue) {
setState(() {
value = newValue;
});
},
)
: const SizedBox.expand(),
);
},
),
),
Expand All @@ -3674,20 +3680,19 @@ void main() {
),
);

// Move Slider.
final TestGesture gesture = await tester
.startGesture(tester.getRect(find.byType(Slider)).centerLeft);
.startGesture(tester.getRect(find.byType(Slider)).center);
await gesture.moveBy(const Offset(1.0, 0.0));
await tester.pumpAndSettle();

// Intentionally not calling `await tester.pumpAndSettle()` to allow drag
// event performed on `Slider` before it is about to get unmounted.
// Hide Slider. Slider will dispose and unmount.
shouldShowSliderListenable.value = false;

await tester.drag(find.byType(Slider), const Offset(1.0, 0.0));
await tester.pumpAndSettle();

expect(value, equals(0.0));

// This is supposed to trigger animation on `Slider` if it is mounted.
await gesture.up();
// Move Slider after unmounted.
await gesture.moveBy(const Offset(1.0, 0.0));
await tester.pumpAndSettle();

expect(tester.takeException(), null);
});
Expand Down

0 comments on commit 06ca902

Please sign in to comment.