Catch and log any exceptions thrown by an app's gesture recognizer callbacks (#6542)
If a recognizer is interrupted by an exception from a callback, it could be
left in an inconsistent state and be unable to process future events
diff --git a/packages/flutter/lib/src/gestures/drag.dart b/packages/flutter/lib/src/gestures/drag.dart
index 4518ac2..befd77a 100644
--- a/packages/flutter/lib/src/gestures/drag.dart
+++ b/packages/flutter/lib/src/gestures/drag.dart
@@ -182,7 +182,7 @@
_initialPosition = event.position;
_pendingDragOffset = Offset.zero;
if (onDown != null)
- onDown(new DragDownDetails(globalPosition: _initialPosition));
+ invokeCallback/*<Null>*/('onDown', () => onDown(new DragDownDetails(globalPosition: _initialPosition)));
}
}
@@ -196,11 +196,11 @@
Offset delta = event.delta;
if (_state == _DragState.accepted) {
if (onUpdate != null) {
- onUpdate(new DragUpdateDetails(
+ invokeCallback/*<Null>*/('onUpdate', () => onUpdate(new DragUpdateDetails(
delta: _getDeltaForDetails(delta),
primaryDelta: _getPrimaryDeltaForDetails(delta),
globalPosition: event.position
- ));
+ )));
}
} else {
_pendingDragOffset += delta;
@@ -218,12 +218,12 @@
Offset delta = _pendingDragOffset;
_pendingDragOffset = Offset.zero;
if (onStart != null)
- onStart(new DragStartDetails(globalPosition: _initialPosition));
+ invokeCallback/*<Null>*/('onStart', () => onStart(new DragStartDetails(globalPosition: _initialPosition)));
if (delta != Offset.zero && onUpdate != null) {
- onUpdate(new DragUpdateDetails(
+ invokeCallback/*<Null>*/('onUpdate', () => onUpdate(new DragUpdateDetails(
delta: _getDeltaForDetails(delta),
primaryDelta: _getPrimaryDeltaForDetails(delta)
- ));
+ )));
}
}
}
@@ -239,7 +239,7 @@
resolve(GestureDisposition.rejected);
_state = _DragState.ready;
if (onCancel != null)
- onCancel();
+ invokeCallback/*<Null>*/('onCancel', onCancel);
return;
}
bool wasAccepted = (_state == _DragState.accepted);
@@ -253,9 +253,9 @@
final Offset pixelsPerSecond = velocity.pixelsPerSecond;
if (pixelsPerSecond.distanceSquared > kMaxFlingVelocity * kMaxFlingVelocity)
velocity = new Velocity(pixelsPerSecond: (pixelsPerSecond / pixelsPerSecond.distance) * kMaxFlingVelocity);
- onEnd(new DragEndDetails(velocity: velocity));
+ invokeCallback/*<Null>*/('onEnd', () => onEnd(new DragEndDetails(velocity: velocity)));
} else {
- onEnd(new DragEndDetails(velocity: Velocity.zero));
+ invokeCallback/*<Null>*/('onEnd', () => onEnd(new DragEndDetails(velocity: Velocity.zero)));
}
}
_velocityTrackers.clear();
diff --git a/packages/flutter/lib/src/gestures/long_press.dart b/packages/flutter/lib/src/gestures/long_press.dart
index 27ba6e4..25685b6 100644
--- a/packages/flutter/lib/src/gestures/long_press.dart
+++ b/packages/flutter/lib/src/gestures/long_press.dart
@@ -26,7 +26,7 @@
void didExceedDeadline() {
resolve(GestureDisposition.accepted);
if (onLongPress != null)
- onLongPress();
+ invokeCallback/*<Null>*/('onLongPress', onLongPress);
}
@override
diff --git a/packages/flutter/lib/src/gestures/multidrag.dart b/packages/flutter/lib/src/gestures/multidrag.dart
index f1b1aaa..86eca4c 100644
--- a/packages/flutter/lib/src/gestures/multidrag.dart
+++ b/packages/flutter/lib/src/gestures/multidrag.dart
@@ -255,7 +255,7 @@
assert(state._pendingDelta != null);
Drag drag;
if (onStart != null)
- drag = onStart(initialPosition);
+ drag = invokeCallback/*<Drag>*/('onStart', () => onStart(initialPosition));
if (drag != null) {
state._startDrag(drag);
} else {
diff --git a/packages/flutter/lib/src/gestures/multitap.dart b/packages/flutter/lib/src/gestures/multitap.dart
index 738a871..5b2d6d9 100644
--- a/packages/flutter/lib/src/gestures/multitap.dart
+++ b/packages/flutter/lib/src/gestures/multitap.dart
@@ -191,7 +191,7 @@
_freezeTracker(tracker);
_trackers.remove(tracker.pointer);
if (onDoubleTap != null)
- onDoubleTap();
+ invokeCallback/*<Null>*/('onDoubleTap', onDoubleTap);
_reset();
}
@@ -359,7 +359,7 @@
longTapDelay: longTapDelay
);
if (onTapDown != null)
- onTapDown(event.pointer, new TapDownDetails(globalPosition: event.position));
+ invokeCallback/*<Null>*/('onTapDown', () => onTapDown(event.pointer, new TapDownDetails(globalPosition: event.position)));
}
@override
@@ -380,19 +380,19 @@
_gestureMap.remove(pointer);
if (resolution == _TapResolution.tap) {
if (onTapUp != null)
- onTapUp(pointer, new TapUpDetails(globalPosition: globalPosition));
+ invokeCallback/*<Null>*/('onTapUp', () => onTapUp(pointer, new TapUpDetails(globalPosition: globalPosition)));
if (onTap != null)
- onTap(pointer);
+ invokeCallback/*<Null>*/('onTap', () => onTap(pointer));
} else {
if (onTapCancel != null)
- onTapCancel(pointer);
+ invokeCallback/*<Null>*/('onTapCancel', () => onTapCancel(pointer));
}
}
void _handleLongTap(int pointer, Point lastPosition) {
assert(_gestureMap.containsKey(pointer));
if (onLongTapDown != null)
- onLongTapDown(pointer, new TapDownDetails(globalPosition: lastPosition));
+ invokeCallback/*<Null>*/('onLongTapDown', () => onLongTapDown(pointer, new TapDownDetails(globalPosition: lastPosition)));
}
@override
diff --git a/packages/flutter/lib/src/gestures/recognizer.dart b/packages/flutter/lib/src/gestures/recognizer.dart
index b354c2c..65e7d87 100644
--- a/packages/flutter/lib/src/gestures/recognizer.dart
+++ b/packages/flutter/lib/src/gestures/recognizer.dart
@@ -6,6 +6,7 @@
import 'dart:collection';
import 'dart:ui' show Point, Offset;
+import 'package:flutter/foundation.dart';
import 'package:meta/meta.dart';
import 'arena.dart';
@@ -16,6 +17,8 @@
export 'pointer_router.dart' show PointerRouter;
+typedef T RecognizerCallback<T>();
+
/// The base class that all GestureRecognizers should inherit from.
///
/// Provides a basic API that can be used by classes that work with
@@ -48,6 +51,28 @@
/// Returns a very short pretty description of the gesture that the
/// recognizer looks for, like 'tap' or 'horizontal drag'.
String toStringShort() => toString();
+
+ /// Invoke a callback provided by the application and log any exceptions.
+ @protected
+ dynamic/*=T*/ invokeCallback/*<T>*/(String name, RecognizerCallback<dynamic/*=T*/> callback) {
+ dynamic/*=T*/ result;
+ try {
+ result = callback();
+ } catch (exception, stack) {
+ FlutterError.reportError(new FlutterErrorDetails(
+ exception: exception,
+ stack: stack,
+ library: 'gesture',
+ context: 'while handling a gesture',
+ informationCollector: (StringBuffer information) {
+ information.writeln('Handler: $name');
+ information.writeln('Recognizer:');
+ information.writeln(' $this');
+ }
+ ));
+ }
+ return result;
+ }
}
/// Base class for gesture recognizers that can only recognize one
diff --git a/packages/flutter/lib/src/gestures/scale.dart b/packages/flutter/lib/src/gestures/scale.dart
index fc0faaf..2ba4f53 100644
--- a/packages/flutter/lib/src/gestures/scale.dart
+++ b/packages/flutter/lib/src/gestures/scale.dart
@@ -180,9 +180,9 @@
final Offset pixelsPerSecond = velocity.pixelsPerSecond;
if (pixelsPerSecond.distanceSquared > kMaxFlingVelocity * kMaxFlingVelocity)
velocity = new Velocity(pixelsPerSecond: (pixelsPerSecond / pixelsPerSecond.distance) * kMaxFlingVelocity);
- onEnd(new ScaleEndDetails(velocity: velocity));
+ invokeCallback/*<Null>*/('onEnd', () => onEnd(new ScaleEndDetails(velocity: velocity)));
} else {
- onEnd(new ScaleEndDetails(velocity: Velocity.zero));
+ invokeCallback/*<Null>*/('onEnd', () => onEnd(new ScaleEndDetails(velocity: Velocity.zero)));
}
}
_state = ScaleState.accepted;
@@ -200,11 +200,11 @@
if (_state == ScaleState.accepted && !configChanged) {
_state = ScaleState.started;
if (onStart != null)
- onStart(new ScaleStartDetails(focalPoint: focalPoint));
+ invokeCallback/*<Null>*/('onStart', () => onStart(new ScaleStartDetails(focalPoint: focalPoint)));
}
if (_state == ScaleState.started && onUpdate != null)
- onUpdate(new ScaleUpdateDetails(scale: _scaleFactor, focalPoint: focalPoint));
+ invokeCallback/*<Null>*/('onUpdate', () => onUpdate(new ScaleUpdateDetails(scale: _scaleFactor, focalPoint: focalPoint)));
}
@override
diff --git a/packages/flutter/lib/src/gestures/tap.dart b/packages/flutter/lib/src/gestures/tap.dart
index 164c710..3d1ebe8 100644
--- a/packages/flutter/lib/src/gestures/tap.dart
+++ b/packages/flutter/lib/src/gestures/tap.dart
@@ -99,7 +99,7 @@
void resolve(GestureDisposition disposition) {
if (_wonArenaForPrimaryPointer && disposition == GestureDisposition.rejected) {
if (onTapCancel != null)
- onTapCancel();
+ invokeCallback/*<Null>*/('onTapCancel', onTapCancel);
_reset();
}
super.resolve(disposition);
@@ -126,7 +126,7 @@
if (pointer == primaryPointer) {
assert(state == GestureRecognizerState.defunct);
if (onTapCancel != null)
- onTapCancel();
+ invokeCallback/*<Null>*/('onTapCancel', onTapCancel);
_reset();
}
}
@@ -134,7 +134,7 @@
void _checkDown() {
if (!_sentTapDown) {
if (onTapDown != null)
- onTapDown(new TapDownDetails(globalPosition: initialPosition));
+ invokeCallback/*<Null>*/('onTapDown', () => onTapDown(new TapDownDetails(globalPosition: initialPosition)));
_sentTapDown = true;
}
}
@@ -143,9 +143,9 @@
if (_wonArenaForPrimaryPointer && _finalPosition != null) {
resolve(GestureDisposition.accepted);
if (onTapUp != null)
- onTapUp(new TapUpDetails(globalPosition: _finalPosition));
+ invokeCallback/*<Null>*/('onTapUp', () => onTapUp(new TapUpDetails(globalPosition: _finalPosition)));
if (onTap != null)
- onTap();
+ invokeCallback/*<Null>*/('onTap', onTap);
_reset();
}
}
diff --git a/packages/flutter/test/gestures/tap_test.dart b/packages/flutter/test/gestures/tap_test.dart
index 966767e..517c608 100644
--- a/packages/flutter/test/gestures/tap_test.dart
+++ b/packages/flutter/test/gestures/tap_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:test/test.dart';
@@ -259,4 +260,28 @@
tap.dispose();
});
+ testGesture('Should log exceptions from callbacks', (GestureTester tester) {
+ TapGestureRecognizer tap = new TapGestureRecognizer();
+
+ tap.onTap = () {
+ throw new Exception(test);
+ };
+
+ FlutterExceptionHandler previousErrorHandler = FlutterError.onError;
+ bool gotError = false;
+ FlutterError.onError = (FlutterErrorDetails details) {
+ gotError = true;
+ };
+
+ tap.addPointer(down1);
+ tester.closeArena(1);
+ tester.route(down1);
+ expect(gotError, isFalse);
+
+ tester.route(up1);
+ expect(gotError, isTrue);
+
+ FlutterError.onError = previousErrorHandler;
+ tap.dispose();
+ });
}