-
Notifications
You must be signed in to change notification settings - Fork 29.7k
Fix potential null exception of OverlayEntry in Slider #124514
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
|
It looks like this pull request may not have tests. Please make sure to add tests before merging. If you need an exemption to this rule, contact Hixie on the #hackers channel in Chat (don't just cc him here, he won't see it! He's on Discord!). If you are not sure if you need tests, consider this rule of thumb: the purpose of a test is to make sure someone doesn't accidentally revert the fix. Ask yourself, is there anything in your PR that you feel it is important we not accidentally revert back to how it was before your fix? Reviewers: Read the Tree Hygiene page and make sure this patch meets those guidelines before LGTMing. |
|
Ready for review |
chunhtai
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I can't think of a way the showValueIndicator can be null, I think there may be something more going on there
| curve: Curves.fastOutSlowIn, | ||
| )..addStatusListener((AnimationStatus status) { | ||
| if (status == AnimationStatus.dismissed && _state.overlayEntry != null) { | ||
| _state.overlayEntry!.remove(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The original code seems fine to me, I think if this become null somehow. Some thing may be broken and we would want to know
|
|
||
| void showValueIndicator() { | ||
| if (overlayEntry == null) { | ||
| overlayEntry = OverlayEntry( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I can't see how it is possible that overlayEntry become null before you insert.
| expect(SchedulerBinding.instance.transientCallbackCount, equals(0)); | ||
| }); | ||
|
|
||
| testWidgets('Should not throw exception when dragged after Slider is disposed', (WidgetTester tester) async { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This test pass even without the change, is this testing a real issue?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@liumcse
It seems that the test passes without the fix on the master.
|
Thanks for updating the PR. I'll take a look on Monday. |
Is this reproducible in a code sample? Here I created a code that disposes Slider with a timer while dragging the thumb. I couldn't reproduce a null pointer exception error. Run the code sample and drag the slider until the page disposes. Check the logs there is no error. code sampleimport 'dart:async';
import 'package:flutter/material.dart';
/// Flutter code sample for [Slider].
void main() => runApp(const SliderApp());
class SliderApp extends StatelessWidget {
const SliderApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
// showSemanticsDebugger: true,
// showPerformanceOverlay: true,
theme: ThemeData(
colorSchemeSeed: const Color(0xff6750a4),
useMaterial3: true,
),
home: const Home(),
);
}
}
class Home extends StatelessWidget {
const Home({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
body: ElevatedButton(
onPressed: () {
Navigator.push(context,
MaterialPageRoute<Widget>(builder: (_) => const SliderExample()));
},
child: Text('Click me'),
),
);
}
}
class SliderExample extends StatefulWidget {
const SliderExample({super.key});
@override
State<SliderExample> createState() => _SliderExampleState();
}
class _SliderExampleState extends State<SliderExample> {
double _currentSliderValue = 20;
@override
Widget build(BuildContext context) {
return Scaffold(
body: Slider(
value: _currentSliderValue,
max: 100,
divisions: 5,
onChangeStart: (double value) {
Timer(const Duration(seconds: 2), () {
Navigator.pop(context);
});
},
label: _currentSliderValue.round().toString(),
onChanged: (double value) {
setState(() {
_currentSliderValue = value;
});
},
),
);
}
}
|
|
cc: @chunhtai |
|
I agree. @liumcse I think we need a repro before we can fix this issue. Otherwise, it would likely to be a workaround that would bite us back in the future. I will be closing this PR, feel free to reopen if you have time to come back to this pr |
This is a follow up to the following pull requests: - #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].*
When
Slideris disposed while user is interacting with the Slider, null pointer exception will be thrown. This PR fixes it by savingoverlayEntryto a local variable when callingshowValueIndicator()instead of using!to force converting potentially null value to non-null value.Also using
?instead of!Indispose()method.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.
Pre-launch Checklist
///).If you need help, consider asking for advice on the #hackers-new channel on Discord.