TextField in a ListView becomes unresponsive (#123734)

TextField in a ListView becomes unresponsive
diff --git a/packages/flutter/lib/src/widgets/tap_and_drag_gestures.dart b/packages/flutter/lib/src/widgets/tap_and_drag_gestures.dart
index 177d625..5d8c337 100644
--- a/packages/flutter/lib/src/widgets/tap_and_drag_gestures.dart
+++ b/packages/flutter/lib/src/widgets/tap_and_drag_gestures.dart
@@ -1218,7 +1218,9 @@
         keysPressedOnDown: keysPressedOnDown,
       );
 
-    invokeCallback<void>('onDragEnd', () => onDragEnd!(endDetails));
+    if (onDragEnd != null) {
+      invokeCallback<void>('onDragEnd', () => onDragEnd!(endDetails));
+    }
 
     _resetTaps();
     _resetDragUpdateThrottle();
diff --git a/packages/flutter/test/widgets/tap_and_drag_gestures_test.dart b/packages/flutter/test/widgets/tap_and_drag_gestures_test.dart
index 32e50e3..b52930c 100644
--- a/packages/flutter/test/widgets/tap_and_drag_gestures_test.dart
+++ b/packages/flutter/test/widgets/tap_and_drag_gestures_test.dart
@@ -2,6 +2,7 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
+import 'package:flutter/foundation.dart';
 import 'package:flutter/gestures.dart';
 import 'package:flutter/widgets.dart';
 import 'package:flutter_test/flutter_test.dart';
@@ -549,4 +550,42 @@
     GestureBinding.instance.gestureArena.sweep(2);
     expect(events, <String>['down#1', 'up#1']);
   });
+
+  // This is a regression test for https://github.com/flutter/flutter/issues/102084.
+  testGesture('Does not call onDragEnd if not provided', (GestureTester tester) {
+    tapAndDrag = TapAndDragGestureRecognizer()
+      ..dragStartBehavior = DragStartBehavior.down
+      ..maxConsecutiveTap = 3
+      ..onTapDown = (TapDragDownDetails details) {
+        events.add('down#${details.consecutiveTapCount}');
+      };
+
+    FlutterErrorDetails? errorDetails;
+    final FlutterExceptionHandler? oldHandler = FlutterError.onError;
+    FlutterError.onError = (FlutterErrorDetails details) {
+      errorDetails = details;
+    };
+    addTearDown(() {
+      FlutterError.onError = oldHandler;
+    });
+
+    tapAndDrag.addPointer(down5);
+    tester.closeArena(5);
+    tester.route(down5);
+    tester.route(move5);
+    tester.route(up5);
+    GestureBinding.instance.gestureArena.sweep(5);
+    expect(events, <String>['down#1']);
+
+    expect(errorDetails, isNull);
+
+    events.clear();
+    tester.async.elapse(const Duration(milliseconds: 1000));
+    tapAndDrag.addPointer(down1);
+    tester.closeArena(1);
+    tester.route(down1);
+    tester.route(up1);
+    GestureBinding.instance.gestureArena.sweep(1);
+    expect(events, <String>['down#1']);
+  });
 }