Unverified Commit f15f2313 authored by Valentin Vignal's avatar Valentin Vignal Committed by GitHub

Fixes `DragTarget` crash if `Draggable.data` is `null` (#133136)

Makes the `data` parameter of `Draggable` non-nullable.

Fixes https://github.com/flutter/flutter/issues/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].*
parent 7646430c
......@@ -652,11 +652,13 @@ class DragTarget<T extends Object> extends StatefulWidget {
final DragTargetWillAcceptWithDetails<T>? onWillAcceptWithDetails;
/// Called when an acceptable piece of data was dropped over this drag target.
/// It will not be called if `data` is `null`.
///
/// Equivalent to [onAcceptWithDetails], but only includes the data.
final DragTargetAccept<T>? onAccept;
/// Called when an acceptable piece of data was dropped over this drag target.
/// It will not be called if `data` is `null`.
///
/// Equivalent to [onAccept], but with information, including the data, in a
/// [DragTargetDetails].
......@@ -666,7 +668,8 @@ class DragTarget<T extends Object> extends StatefulWidget {
/// the target.
final DragTargetLeave<T>? onLeave;
/// Called when a [Draggable] moves within this [DragTarget].
/// Called when a [Draggable] moves within this [DragTarget]. It will not be
/// called if `data` is `null`.
///
/// This includes entering and leaving the target.
final DragTargetMove<T>? onMove;
......@@ -707,6 +710,7 @@ class _DragTargetState<T extends Object> extends State<DragTarget<T>> {
(widget.onWillAccept != null &&
widget.onWillAccept!(avatar.data as T?)) ||
(widget.onWillAcceptWithDetails != null &&
avatar.data != null &&
widget.onWillAcceptWithDetails!(DragTargetDetails<T>(data: avatar.data! as T, offset: avatar._lastOffset!)));
if (resolvedWillAccept) {
setState(() {
......@@ -741,12 +745,14 @@ class _DragTargetState<T extends Object> extends State<DragTarget<T>> {
setState(() {
_candidateAvatars.remove(avatar);
});
if (avatar.data != null) {
widget.onAccept?.call(avatar.data! as T);
widget.onAcceptWithDetails?.call(DragTargetDetails<T>(data: avatar.data! as T, offset: avatar._lastOffset!));
}
}
void didMove(_DragAvatar<Object> avatar) {
if (!mounted) {
if (!mounted || avatar.data == null) {
return;
}
widget.onMove?.call(DragTargetDetails<T>(data: avatar.data! as T, offset: avatar._lastOffset!));
......
......@@ -395,6 +395,47 @@ void main() {
expect(targetMoveCount['Target 2'], equals(1));
});
testWidgetsWithLeakTracking('Drag and drop - onMove is not called if moved with null data', (WidgetTester tester) async {
bool onMoveCalled = false;
await tester.pumpWidget(MaterialApp(
home: Column(
children: <Widget>[
const Draggable<int>(
feedback: Text('Dragging'),
child: Text('Source'),
),
DragTarget<int>(
builder: (BuildContext context, List<int?> data, List<dynamic> rejects) {
return const SizedBox(height: 100.0, child: Text('Target'));
},
onMove: (DragTargetDetails<dynamic> details) {
onMoveCalled = true;
},
),
],
),
));
expect(onMoveCalled, isFalse);
final Offset firstLocation = tester.getCenter(find.text('Source'));
final TestGesture gesture = await tester.startGesture(firstLocation, pointer: 7);
await tester.pump();
expect(onMoveCalled, isFalse);
final Offset secondLocation = tester.getCenter(find.text('Target'));
await gesture.moveTo(secondLocation);
await tester.pump();
expect(onMoveCalled, isFalse);
await gesture.up();
await tester.pump();
expect(onMoveCalled, isFalse);
});
testWidgetsWithLeakTracking('Drag and drop - dragging over button', (WidgetTester tester) async {
final List<String> events = <String>[];
Offset firstLocation, secondLocation;
......@@ -2392,6 +2433,68 @@ void main() {
expect(find.text('Target'), findsOneWidget);
});
testWidgetsWithLeakTracking('Drag and drop - onAccept is not called if dropped with null data', (WidgetTester tester) async {
bool onAcceptCalled = false;
bool onAcceptWithDetailsCalled = false;
await tester.pumpWidget(MaterialApp(
home: Column(
children: <Widget>[
const Draggable<int>(
feedback: Text('Dragging'),
child: Text('Source'),
),
DragTarget<int>(
builder: (BuildContext context, List<int?> data, List<dynamic> rejects) {
return const SizedBox(height: 100.0, child: Text('Target'));
},
onAccept: (int data) {
onAcceptCalled = true;
},
onAcceptWithDetails: (DragTargetDetails<int> details) {
onAcceptWithDetailsCalled =true;
},
),
],
),
));
expect(onAcceptCalled, isFalse);
expect(onAcceptWithDetailsCalled, isFalse);
expect(find.text('Source'), findsOneWidget);
expect(find.text('Dragging'), findsNothing);
expect(find.text('Target'), findsOneWidget);
final Offset firstLocation = tester.getCenter(find.text('Source'));
final TestGesture gesture = await tester.startGesture(firstLocation, pointer: 7);
await tester.pump();
expect(onAcceptCalled, isFalse);
expect(onAcceptWithDetailsCalled, isFalse);
expect(find.text('Source'), findsOneWidget);
expect(find.text('Dragging'), findsOneWidget);
expect(find.text('Target'), findsOneWidget);
final Offset secondLocation = tester.getCenter(find.text('Target'));
await gesture.moveTo(secondLocation);
await tester.pump();
expect(onAcceptCalled, isFalse);
expect(onAcceptWithDetailsCalled, isFalse);
expect(find.text('Source'), findsOneWidget);
expect(find.text('Dragging'), findsOneWidget);
expect(find.text('Target'), findsOneWidget);
await gesture.up();
await tester.pump();
expect(onAcceptCalled, isFalse, reason: 'onAccept should not be called when data is null');
expect(onAcceptWithDetailsCalled, isFalse, reason: 'onAcceptWithDetails should not be called when data is null');
expect(find.text('Source'), findsOneWidget);
expect(find.text('Dragging'), findsNothing);
expect(find.text('Target'), findsOneWidget);
});
testWidgetsWithLeakTracking('Draggable disposes recognizer', (WidgetTester tester) async {
late final OverlayEntry entry;
addTearDown(() => entry..remove()..dispose());
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment