Skip to content

Commit f15f231

Browse files
Fixes DragTarget crash if Draggable.data is null (#133136)
Makes the `data` parameter of `Draggable` non-nullable. Fixes flutter/flutter#84816 *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 7646430 commit f15f231

File tree

2 files changed

+113
-4
lines changed

2 files changed

+113
-4
lines changed

packages/flutter/lib/src/widgets/drag_target.dart

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -652,11 +652,13 @@ class DragTarget<T extends Object> extends StatefulWidget {
652652
final DragTargetWillAcceptWithDetails<T>? onWillAcceptWithDetails;
653653

654654
/// Called when an acceptable piece of data was dropped over this drag target.
655+
/// It will not be called if `data` is `null`.
655656
///
656657
/// Equivalent to [onAcceptWithDetails], but only includes the data.
657658
final DragTargetAccept<T>? onAccept;
658659

659660
/// Called when an acceptable piece of data was dropped over this drag target.
661+
/// It will not be called if `data` is `null`.
660662
///
661663
/// Equivalent to [onAccept], but with information, including the data, in a
662664
/// [DragTargetDetails].
@@ -666,7 +668,8 @@ class DragTarget<T extends Object> extends StatefulWidget {
666668
/// the target.
667669
final DragTargetLeave<T>? onLeave;
668670

669-
/// Called when a [Draggable] moves within this [DragTarget].
671+
/// Called when a [Draggable] moves within this [DragTarget]. It will not be
672+
/// called if `data` is `null`.
670673
///
671674
/// This includes entering and leaving the target.
672675
final DragTargetMove<T>? onMove;
@@ -707,6 +710,7 @@ class _DragTargetState<T extends Object> extends State<DragTarget<T>> {
707710
(widget.onWillAccept != null &&
708711
widget.onWillAccept!(avatar.data as T?)) ||
709712
(widget.onWillAcceptWithDetails != null &&
713+
avatar.data != null &&
710714
widget.onWillAcceptWithDetails!(DragTargetDetails<T>(data: avatar.data! as T, offset: avatar._lastOffset!)));
711715
if (resolvedWillAccept) {
712716
setState(() {
@@ -741,12 +745,14 @@ class _DragTargetState<T extends Object> extends State<DragTarget<T>> {
741745
setState(() {
742746
_candidateAvatars.remove(avatar);
743747
});
744-
widget.onAccept?.call(avatar.data! as T);
745-
widget.onAcceptWithDetails?.call(DragTargetDetails<T>(data: avatar.data! as T, offset: avatar._lastOffset!));
748+
if (avatar.data != null) {
749+
widget.onAccept?.call(avatar.data! as T);
750+
widget.onAcceptWithDetails?.call(DragTargetDetails<T>(data: avatar.data! as T, offset: avatar._lastOffset!));
751+
}
746752
}
747753

748754
void didMove(_DragAvatar<Object> avatar) {
749-
if (!mounted) {
755+
if (!mounted || avatar.data == null) {
750756
return;
751757
}
752758
widget.onMove?.call(DragTargetDetails<T>(data: avatar.data! as T, offset: avatar._lastOffset!));

packages/flutter/test/widgets/draggable_test.dart

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -395,6 +395,47 @@ void main() {
395395
expect(targetMoveCount['Target 2'], equals(1));
396396
});
397397

398+
testWidgetsWithLeakTracking('Drag and drop - onMove is not called if moved with null data', (WidgetTester tester) async {
399+
bool onMoveCalled = false;
400+
401+
await tester.pumpWidget(MaterialApp(
402+
home: Column(
403+
children: <Widget>[
404+
const Draggable<int>(
405+
feedback: Text('Dragging'),
406+
child: Text('Source'),
407+
),
408+
DragTarget<int>(
409+
builder: (BuildContext context, List<int?> data, List<dynamic> rejects) {
410+
return const SizedBox(height: 100.0, child: Text('Target'));
411+
},
412+
onMove: (DragTargetDetails<dynamic> details) {
413+
onMoveCalled = true;
414+
},
415+
),
416+
],
417+
),
418+
));
419+
420+
expect(onMoveCalled, isFalse);
421+
422+
final Offset firstLocation = tester.getCenter(find.text('Source'));
423+
final TestGesture gesture = await tester.startGesture(firstLocation, pointer: 7);
424+
await tester.pump();
425+
426+
expect(onMoveCalled, isFalse);
427+
428+
final Offset secondLocation = tester.getCenter(find.text('Target'));
429+
await gesture.moveTo(secondLocation);
430+
await tester.pump();
431+
432+
expect(onMoveCalled, isFalse);
433+
await gesture.up();
434+
await tester.pump();
435+
436+
expect(onMoveCalled, isFalse);
437+
});
438+
398439
testWidgetsWithLeakTracking('Drag and drop - dragging over button', (WidgetTester tester) async {
399440
final List<String> events = <String>[];
400441
Offset firstLocation, secondLocation;
@@ -2392,6 +2433,68 @@ void main() {
23922433
expect(find.text('Target'), findsOneWidget);
23932434
});
23942435

2436+
testWidgetsWithLeakTracking('Drag and drop - onAccept is not called if dropped with null data', (WidgetTester tester) async {
2437+
bool onAcceptCalled = false;
2438+
bool onAcceptWithDetailsCalled = false;
2439+
2440+
await tester.pumpWidget(MaterialApp(
2441+
home: Column(
2442+
children: <Widget>[
2443+
const Draggable<int>(
2444+
feedback: Text('Dragging'),
2445+
child: Text('Source'),
2446+
),
2447+
DragTarget<int>(
2448+
builder: (BuildContext context, List<int?> data, List<dynamic> rejects) {
2449+
return const SizedBox(height: 100.0, child: Text('Target'));
2450+
},
2451+
onAccept: (int data) {
2452+
onAcceptCalled = true;
2453+
},
2454+
onAcceptWithDetails: (DragTargetDetails<int> details) {
2455+
onAcceptWithDetailsCalled =true;
2456+
},
2457+
),
2458+
],
2459+
),
2460+
));
2461+
2462+
expect(onAcceptCalled, isFalse);
2463+
expect(onAcceptWithDetailsCalled, isFalse);
2464+
expect(find.text('Source'), findsOneWidget);
2465+
expect(find.text('Dragging'), findsNothing);
2466+
expect(find.text('Target'), findsOneWidget);
2467+
2468+
final Offset firstLocation = tester.getCenter(find.text('Source'));
2469+
final TestGesture gesture = await tester.startGesture(firstLocation, pointer: 7);
2470+
await tester.pump();
2471+
2472+
expect(onAcceptCalled, isFalse);
2473+
expect(onAcceptWithDetailsCalled, isFalse);
2474+
expect(find.text('Source'), findsOneWidget);
2475+
expect(find.text('Dragging'), findsOneWidget);
2476+
expect(find.text('Target'), findsOneWidget);
2477+
2478+
final Offset secondLocation = tester.getCenter(find.text('Target'));
2479+
await gesture.moveTo(secondLocation);
2480+
await tester.pump();
2481+
2482+
expect(onAcceptCalled, isFalse);
2483+
expect(onAcceptWithDetailsCalled, isFalse);
2484+
expect(find.text('Source'), findsOneWidget);
2485+
expect(find.text('Dragging'), findsOneWidget);
2486+
expect(find.text('Target'), findsOneWidget);
2487+
2488+
await gesture.up();
2489+
await tester.pump();
2490+
2491+
expect(onAcceptCalled, isFalse, reason: 'onAccept should not be called when data is null');
2492+
expect(onAcceptWithDetailsCalled, isFalse, reason: 'onAcceptWithDetails should not be called when data is null');
2493+
expect(find.text('Source'), findsOneWidget);
2494+
expect(find.text('Dragging'), findsNothing);
2495+
expect(find.text('Target'), findsOneWidget);
2496+
});
2497+
23952498
testWidgetsWithLeakTracking('Draggable disposes recognizer', (WidgetTester tester) async {
23962499
late final OverlayEntry entry;
23972500
addTearDown(() => entry..remove()..dispose());

0 commit comments

Comments
 (0)