Improve our scroll physics on iOS (#5340)

Changes in this patch:
- iOS now uses a different scrollDrag constant than Android.
   - ScrollConfigurationDelegate now knows about target platforms.
   - ScrollBehaviors now know about target platforms.
   - RawInputLine now has to be told what platform it's targetting.
   - PageableList now has a concept of target platform.
- make debugPrintStack filter its stack.
   - move debugPrintStack to `assertions.dart`.
- add support for limiting the number of frames to debugPrintStack.
- make defaultTargetPlatform default to android in test environments.
- remove OverscrollStyle and MaterialApp's overscrollStyle argument. You
  can now control the overscroll style using Theme.platform.
- the default scroll configuration is now private to avoid people
  relying on the defaultTargetPlatform getter in their subclasses (since
  they really should use Theme.of(context).platform).
- fix some typos I noticed in some tests.
- added a test for flinging scrollables, that checks that the behavior
  differs on the two target platforms.
- made flingFrom and fling in the test API pump the frames.
- added more docs to the test API.
- made the TestAsyncUtils.guard() method report uncaught errors to help
  debug errors when using that API.
diff --git a/packages/flutter/lib/src/foundation/assertions.dart b/packages/flutter/lib/src/foundation/assertions.dart
index 4457dbd..f87665c 100644
--- a/packages/flutter/lib/src/foundation/assertions.dart
+++ b/packages/flutter/lib/src/foundation/assertions.dart
@@ -326,3 +326,17 @@
       onError(details);
   }
 }
+
+/// Dump the current stack to the console using [debugPrint] and
+/// [FlutterError.defaultStackFilter].
+///
+/// The current stack is obtained using [StackTrace.current].
+///
+/// The `maxFrames` argument can be given to limit the stack to the given number
+/// of lines. By default, all non-filtered stack lines are shown.
+void debugPrintStack({ int maxFrames }) {
+  List<String> lines = StackTrace.current.toString().trimRight().split('\n');
+  if (maxFrames != null)
+    lines = lines.take(maxFrames);
+  debugPrint(FlutterError.defaultStackFilter(lines).join('\n'));
+}
diff --git a/packages/flutter/lib/src/foundation/platform.dart b/packages/flutter/lib/src/foundation/platform.dart
index 3881055..4854155 100644
--- a/packages/flutter/lib/src/foundation/platform.dart
+++ b/packages/flutter/lib/src/foundation/platform.dart
@@ -15,15 +15,33 @@
   iOS,
 }
 
-/// The [TargetPlatform] that matches the platform on which the framework is currently executing.
+/// The [TargetPlatform] that matches the platform on which the framework is
+/// currently executing.
+///
+/// In a test environment, the platform returned is [TargetPlatform.android]
+/// regardless of the host platform. (Android was chosen because the tests were
+/// originally written assuming Android-like behavior, and we added platform
+/// adaptations for iOS later). Tests can check iOS behavior by using the
+/// platform override APIs (like in [ThemeData.platform] in the material
+/// library).
 TargetPlatform get defaultTargetPlatform {
-  if (Platform.isIOS || Platform.isMacOS)
-    return TargetPlatform.iOS;
-  if (Platform.isAndroid || Platform.isLinux)
-    return TargetPlatform.android;
-  throw new FlutterError(
-    'Unknown platform\n'
-    '${Platform.operatingSystem} was not recognized as a target platform. '
-    'Consider updating the list of TargetPlatforms to include this platform.'
-  );
+  TargetPlatform result;
+  if (Platform.isIOS || Platform.isMacOS) {
+    result = TargetPlatform.iOS;
+  } else if (Platform.isAndroid || Platform.isLinux) {
+    result = TargetPlatform.android;
+  }
+  assert(() {
+    if (Platform.environment.containsKey('FLUTTER_TEST'))
+      result = TargetPlatform.android;
+    return true;
+  });
+  if (result == null) {
+    throw new FlutterError(
+      'Unknown platform.\n'
+      '${Platform.operatingSystem} was not recognized as a target platform. '
+      'Consider updating the list of TargetPlatforms to include this platform.'
+    );
+  }
+  return result;
 }
diff --git a/packages/flutter/lib/src/foundation/print.dart b/packages/flutter/lib/src/foundation/print.dart
index c9aecad..2d15441 100644
--- a/packages/flutter/lib/src/foundation/print.dart
+++ b/packages/flutter/lib/src/foundation/print.dart
@@ -160,10 +160,3 @@
     }
   }
 }
-
-/// Dump the current stack to the console using [debugPrint].
-///
-/// The current stack is obtained using [StackTrace.current].
-void debugPrintStack() {
-  debugPrint(StackTrace.current.toString());
-}
diff --git a/packages/flutter/lib/src/material/app.dart b/packages/flutter/lib/src/material/app.dart
index d79d3d2..094b611 100644
--- a/packages/flutter/lib/src/material/app.dart
+++ b/packages/flutter/lib/src/material/app.dart
@@ -23,15 +23,6 @@
   decorationStyle: TextDecorationStyle.double
 );
 
-/// The visual and interaction design for overscroll.
-enum OverscrollStyle {
-  /// Overscrolls are clamped and indicated with a glow.
-  glow,
-
-  /// Overscrolls are not clamped and indicated with elastic physics.
-  bounce
-}
-
 /// An application that uses material design.
 ///
 /// A convenience widget that wraps a number of widgets that are commonly
@@ -60,7 +51,6 @@
     this.theme,
     this.home,
     this.routes: const <String, WidgetBuilder>{},
-    this.overscrollStyle,
     this.onGenerateRoute,
     this.onLocaleChanged,
     this.debugShowMaterialGrid: false,
@@ -112,11 +102,6 @@
   /// build the page instead.
   final Map<String, WidgetBuilder> routes;
 
-  /// The visual and interaction design for overscroll.
-  ///
-  /// Defaults to being adapted to the current [TargetPlatform].
-  final OverscrollStyle overscrollStyle;
-
   /// The route generator callback used when the app is navigated to a
   /// named route.
   final RouteFactory onGenerateRoute;
@@ -158,13 +143,34 @@
   _MaterialAppState createState() => new _MaterialAppState();
 }
 
-class _IndicatorScrollConfigurationDelegate extends ScrollConfigurationDelegate {
+class _ScrollLikeCupertinoDelegate extends ScrollConfigurationDelegate {
+  const _ScrollLikeCupertinoDelegate();
+
   @override
-  Widget wrapScrollWidget(Widget scrollWidget) => new OverscrollIndicator(child: scrollWidget);
+  TargetPlatform get platform => TargetPlatform.iOS;
+
+  @override
+  ExtentScrollBehavior createScrollBehavior() => new OverscrollWhenScrollableBehavior(platform: TargetPlatform.iOS);
+
+  @override
+  bool updateShouldNotify(ScrollConfigurationDelegate old) => false;
 }
 
-final ScrollConfigurationDelegate _glowScroll = new _IndicatorScrollConfigurationDelegate();
-final ScrollConfigurationDelegate _bounceScroll = new ScrollConfigurationDelegate();
+class _ScrollLikeMountainViewDelegate extends ScrollConfigurationDelegate {
+  const _ScrollLikeMountainViewDelegate();
+
+  @override
+  TargetPlatform get platform => TargetPlatform.android;
+
+  @override
+  ExtentScrollBehavior createScrollBehavior() => new OverscrollWhenScrollableBehavior(platform: TargetPlatform.android);
+
+  @override
+  Widget wrapScrollWidget(Widget scrollWidget) => new OverscrollIndicator(child: scrollWidget);
+
+  @override
+  bool updateShouldNotify(ScrollConfigurationDelegate old) => false;
+}
 
 class _MaterialAppState extends State<MaterialApp> {
   HeroController _heroController;
@@ -195,19 +201,11 @@
   }
 
   ScrollConfigurationDelegate _getScrollDelegate(TargetPlatform platform) {
-    if (config.overscrollStyle != null) {
-      switch (config.overscrollStyle) {
-        case OverscrollStyle.glow:
-          return _glowScroll;
-        case OverscrollStyle.bounce:
-          return _bounceScroll;
-      }
-    }
     switch (platform) {
       case TargetPlatform.android:
-        return _glowScroll;
+        return const _ScrollLikeMountainViewDelegate();
       case TargetPlatform.iOS:
-        return _bounceScroll;
+        return const _ScrollLikeCupertinoDelegate();
     }
     return null;
   }
diff --git a/packages/flutter/lib/src/material/drop_down.dart b/packages/flutter/lib/src/material/drop_down.dart
index 6826960..2b3bf40 100644
--- a/packages/flutter/lib/src/material/drop_down.dart
+++ b/packages/flutter/lib/src/material/drop_down.dart
@@ -78,12 +78,21 @@
 // Do not use the platform-specific default scroll configuration.
 // Dropdown menus should never overscroll or display an overscroll indicator.
 class _DropDownScrollConfigurationDelegate extends ScrollConfigurationDelegate {
-  const _DropDownScrollConfigurationDelegate();
+  const _DropDownScrollConfigurationDelegate(this._platform);
+
+  @override
+  TargetPlatform get platform => _platform;
+  final TargetPlatform _platform;
+
+  @override
+  ExtentScrollBehavior createScrollBehavior() => new OverscrollWhenScrollableBehavior(platform: platform);
 
   @override
   Widget wrapScrollWidget(Widget scrollWidget) => new ClampOverscrolls(value: true, child: scrollWidget);
+
+  @override
+  bool updateShouldNotify(ScrollConfigurationDelegate old) => platform != old.platform;
 }
-final ScrollConfigurationDelegate _dropDownScroll = const _DropDownScrollConfigurationDelegate();
 
 class _DropDownMenu<T> extends StatefulWidget {
   _DropDownMenu({
@@ -170,7 +179,7 @@
           type: MaterialType.transparency,
           textStyle: route.style,
           child: new ScrollConfiguration(
-            delegate: _dropDownScroll,
+            delegate: new _DropDownScrollConfigurationDelegate(Theme.of(context).platform),
             child: new Scrollbar(
               child: new ScrollableList(
                 scrollableKey: config.route.scrollableKey,
diff --git a/packages/flutter/lib/src/material/input.dart b/packages/flutter/lib/src/material/input.dart
index df4365f..468de74 100644
--- a/packages/flutter/lib/src/material/input.dart
+++ b/packages/flutter/lib/src/material/input.dart
@@ -205,6 +205,7 @@
         selectionColor: themeData.textSelectionColor,
         selectionHandleBuilder: buildTextSelectionHandle,
         selectionToolbarBuilder: buildTextSelectionToolbar,
+        platform: Theme.of(context).platform,
         keyboardType: config.keyboardType,
         onChanged: onChanged,
         onSubmitted: onSubmitted
diff --git a/packages/flutter/lib/src/material/tabs.dart b/packages/flutter/lib/src/material/tabs.dart
index a85f2d8..d28d055 100644
--- a/packages/flutter/lib/src/material/tabs.dart
+++ b/packages/flutter/lib/src/material/tabs.dart
@@ -1090,10 +1090,13 @@
 
   @override
   ExtentScrollBehavior get scrollBehavior {
-    _boundedBehavior ??= new BoundedBehavior();
+    _boundedBehavior ??= new BoundedBehavior(platform: platform);
     return _boundedBehavior;
   }
 
+  @override
+  TargetPlatform get platform => Theme.of(context).platform;
+
   void _initSelection(TabBarSelectionState<T> selection) {
     _selection = selection;
     if (_selection != null) {
diff --git a/packages/flutter/lib/src/widgets/editable.dart b/packages/flutter/lib/src/widgets/editable.dart
index f224fa4..b10e1e6 100644
--- a/packages/flutter/lib/src/widgets/editable.dart
+++ b/packages/flutter/lib/src/widgets/editable.dart
@@ -169,6 +169,7 @@
     this.selectionColor,
     this.selectionHandleBuilder,
     this.selectionToolbarBuilder,
+    @required this.platform,
     this.keyboardType,
     this.onChanged,
     this.onSubmitted
@@ -206,6 +207,12 @@
   /// text selection (e.g. copy and paste).
   final TextSelectionToolbarBuilder selectionToolbarBuilder;
 
+  /// The platform whose behavior should be approximated, in particular
+  /// for scroll physics. (See [ScrollBehavior.platform].)
+  ///
+  /// Must not be null.
+  final TargetPlatform platform;
+
   /// The type of keyboard to use for editing the text.
   final KeyboardType keyboardType;
 
@@ -229,7 +236,7 @@
   TextSelectionOverlay _selectionOverlay;
 
   @override
-  ScrollBehavior<double, double> createScrollBehavior() => new BoundedBehavior();
+  ScrollBehavior<double, double> createScrollBehavior() => new BoundedBehavior(platform: config.platform);
 
   @override
   BoundedBehavior get scrollBehavior => super.scrollBehavior;
diff --git a/packages/flutter/lib/src/widgets/pageable_list.dart b/packages/flutter/lib/src/widgets/pageable_list.dart
index 611f60f..a6ab213 100644
--- a/packages/flutter/lib/src/widgets/pageable_list.dart
+++ b/packages/flutter/lib/src/widgets/pageable_list.dart
@@ -9,6 +9,7 @@
 import 'basic.dart';
 import 'framework.dart';
 import 'scroll_behavior.dart';
+import 'scroll_configuration.dart';
 import 'scrollable.dart';
 import 'virtual_viewport.dart';
 
@@ -292,13 +293,18 @@
   @override
   ExtentScrollBehavior get scrollBehavior {
     if (config.itemsWrap) {
-      _unboundedBehavior ??= new UnboundedBehavior();
+      _unboundedBehavior ??= new UnboundedBehavior(platform: platform);
       return _unboundedBehavior;
     }
-    _overscrollBehavior ??= new OverscrollBehavior();
+    _overscrollBehavior ??= new OverscrollBehavior(platform: platform);
     return _overscrollBehavior;
   }
 
+  /// Returns the style of scrolling to use.
+  ///
+  /// By default, defers to the nearest [ScrollConfiguration].
+  TargetPlatform get platform => ScrollConfiguration.of(context)?.platform;
+
   @override
   ExtentScrollBehavior createScrollBehavior() => scrollBehavior;
 
diff --git a/packages/flutter/lib/src/widgets/scroll_behavior.dart b/packages/flutter/lib/src/widgets/scroll_behavior.dart
index f286be3..51dc774 100644
--- a/packages/flutter/lib/src/widgets/scroll_behavior.dart
+++ b/packages/flutter/lib/src/widgets/scroll_behavior.dart
@@ -4,21 +4,49 @@
 
 import 'dart:math' as math;
 
+import 'package:flutter/foundation.dart';
 import 'package:flutter/physics.dart';
 import 'package:meta/meta.dart';
 
-const double _kScrollDrag = 0.025;
+export 'package:flutter/foundation.dart' show TargetPlatform;
+
+const double _kScrollDragMountainView = 0.025;
+const double _kScrollDragCupertino = 0.125;
+final SpringDescription _kScrollSpring = new SpringDescription.withDampingRatio(mass: 0.5, springConstant: 100.0, ratio: 1.1);
+
+Simulation _createScrollSimulation(double position, double velocity, double minScrollOffset, double maxScrollOffset, double scrollDrag) {
+  return new ScrollSimulation(position, velocity, minScrollOffset, maxScrollOffset, _kScrollSpring, scrollDrag);
+}
+
+Simulation _createSnapScrollSimulation(double startOffset, double endOffset, double startVelocity, double endVelocity) {
+  return new FrictionSimulation.through(startOffset, endOffset, startVelocity, endVelocity);
+}
 
 // TODO(hansmuller): Simplify these classes. We're no longer using the ScrollBehavior<T, U>
 // base class directly. Only LazyBlock uses BoundedBehavior's updateExtents minScrollOffset
 // parameter; simpler to move that into ExtentScrollBehavior.  All of the classes should
-// be called FooScrollBehavior.
+// be called FooScrollBehavior. See https://github.com/flutter/flutter/issues/5281
 
 /// An interface for controlling the behavior of scrollable widgets.
 ///
 /// The type argument T is the type that describes the scroll offset.
 /// The type argument U is the type that describes the scroll velocity.
 abstract class ScrollBehavior<T, U> {
+  /// Abstract const constructor. This constructor enables subclasses to provide
+  /// const constructors so that they can be used in const expressions.
+  ///
+  /// The [platform] must not be null.
+  const ScrollBehavior({
+    @required this.platform
+  });
+
+  /// The platform for which physics constants should be approximated.
+  ///
+  /// This is what makes flings go further on iOS than Android.
+  ///
+  /// Must not be null.
+  final TargetPlatform platform;
+
   /// Returns a simulation that propels the scrollOffset.
   ///
   /// This function is called when a drag gesture ends.
@@ -41,6 +69,19 @@
   /// Whether this scroll behavior currently permits scrolling
   bool get isScrollable => true;
 
+  /// The scroll drag constant to use for physics simulations created by this
+  /// ScrollBehavior.
+  double get scrollDrag {
+    assert(platform != null);
+    switch (platform) {
+      case TargetPlatform.android:
+        return _kScrollDragMountainView;
+      case TargetPlatform.iOS:
+        return _kScrollDragCupertino;
+    }
+    return null;
+  }
+
   @override
   String toString() {
     List<String> description = <String>[];
@@ -64,8 +105,15 @@
   /// Creates a scroll behavior for a scrollable widget with linear extent.
   /// We start with an INFINITE contentExtent so that we don't accidentally
   /// clamp a scrollOffset until we receive an accurate value in updateExtents.
-  ExtentScrollBehavior({ double contentExtent: double.INFINITY, double containerExtent: 0.0 })
-    : _contentExtent = contentExtent, _containerExtent = containerExtent;
+  ///
+  /// The extents and the [platform] must not be null.
+  ExtentScrollBehavior({
+    double contentExtent: double.INFINITY,
+    double containerExtent: 0.0,
+    @required TargetPlatform platform
+  }) : _contentExtent = contentExtent,
+       _containerExtent = containerExtent,
+       super(platform: platform);
 
   /// The linear extent of the content inside the scrollable widget.
   double get contentExtent => _contentExtent;
@@ -115,9 +163,14 @@
   BoundedBehavior({
     double contentExtent: double.INFINITY,
     double containerExtent: 0.0,
-    double minScrollOffset: 0.0
+    double minScrollOffset: 0.0,
+    @required TargetPlatform platform
   }) : _minScrollOffset = minScrollOffset,
-       super(contentExtent: contentExtent, containerExtent: containerExtent);
+       super(
+         contentExtent: contentExtent,
+         containerExtent: containerExtent,
+         platform: platform
+       );
 
   double _minScrollOffset;
 
@@ -151,25 +204,23 @@
   }
 }
 
-Simulation _createScrollSimulation(double position, double velocity, double minScrollOffset, double maxScrollOffset) {
-  final SpringDescription spring = new SpringDescription.withDampingRatio(mass: 1.0, springConstant: 170.0, ratio: 1.1);
-  return new ScrollSimulation(position, velocity, minScrollOffset, maxScrollOffset, spring, _kScrollDrag);
-}
-
-Simulation _createSnapScrollSimulation(double startOffset, double endOffset, double startVelocity, double endVelocity) {
-  return new FrictionSimulation.through(startOffset, endOffset, startVelocity, endVelocity);
-}
-
-/// A scroll behavior that does not prevent the user from exeeding scroll bounds.
+/// A scroll behavior that does not prevent the user from exceeding scroll bounds.
 class UnboundedBehavior extends ExtentScrollBehavior {
   /// Creates a scroll behavior with no scrolling limits.
-  UnboundedBehavior({ double contentExtent: double.INFINITY, double containerExtent: 0.0 })
-    : super(contentExtent: contentExtent, containerExtent: containerExtent);
+  UnboundedBehavior({
+    double contentExtent: double.INFINITY,
+    double containerExtent: 0.0,
+    @required TargetPlatform platform
+  }) : super(
+    contentExtent: contentExtent,
+    containerExtent: containerExtent,
+    platform: platform
+  );
 
   @override
   Simulation createScrollSimulation(double position, double velocity) {
     return new BoundedFrictionSimulation(
-      _kScrollDrag, position, velocity, double.NEGATIVE_INFINITY, double.INFINITY
+      scrollDrag, position, velocity, double.NEGATIVE_INFINITY, double.INFINITY
     );
   }
 
@@ -193,12 +244,21 @@
 /// A scroll behavior that lets the user scroll beyond the scroll bounds with some resistance.
 class OverscrollBehavior extends BoundedBehavior {
   /// Creates a scroll behavior that resists, but does not prevent, scrolling beyond its limits.
-  OverscrollBehavior({ double contentExtent: double.INFINITY, double containerExtent: 0.0, double minScrollOffset: 0.0 })
-    : super(contentExtent: contentExtent, containerExtent: containerExtent, minScrollOffset: minScrollOffset);
+  OverscrollBehavior({
+    double contentExtent: double.INFINITY,
+    double containerExtent: 0.0,
+    double minScrollOffset: 0.0,
+    @required TargetPlatform platform
+  }) : super(
+    contentExtent: contentExtent,
+    containerExtent: containerExtent,
+    minScrollOffset: minScrollOffset,
+    platform: platform
+  );
 
   @override
   Simulation createScrollSimulation(double position, double velocity) {
-    return _createScrollSimulation(position, velocity, minScrollOffset, maxScrollOffset);
+    return _createScrollSimulation(position, velocity, minScrollOffset, maxScrollOffset, scrollDrag);
   }
 
   @override
@@ -227,8 +287,17 @@
 /// A scroll behavior that lets the user scroll beyond the scroll bounds only when the bounds are disjoint.
 class OverscrollWhenScrollableBehavior extends OverscrollBehavior {
   /// Creates a scroll behavior that allows overscrolling only when some amount of scrolling is already possible.
-  OverscrollWhenScrollableBehavior({ double contentExtent: double.INFINITY, double containerExtent: 0.0, double minScrollOffset: 0.0 })
-    : super(contentExtent: contentExtent, containerExtent: containerExtent, minScrollOffset: minScrollOffset);
+  OverscrollWhenScrollableBehavior({
+    double contentExtent: double.INFINITY,
+    double containerExtent: 0.0,
+    double minScrollOffset: 0.0,
+    @required TargetPlatform platform
+  }) : super(
+    contentExtent: contentExtent,
+    containerExtent: containerExtent,
+    minScrollOffset: minScrollOffset,
+    platform: platform
+  );
 
   @override
   bool get isScrollable => contentExtent > containerExtent;
diff --git a/packages/flutter/lib/src/widgets/scroll_configuration.dart b/packages/flutter/lib/src/widgets/scroll_configuration.dart
index ed54658..dda1ca3 100644
--- a/packages/flutter/lib/src/widgets/scroll_configuration.dart
+++ b/packages/flutter/lib/src/widgets/scroll_configuration.dart
@@ -3,6 +3,7 @@
 // found in the LICENSE file.
 
 import 'package:meta/meta.dart';
+import 'package:flutter/foundation.dart';
 
 import 'framework.dart';
 import 'scroll_behavior.dart';
@@ -10,13 +11,18 @@
 /// Controls how [Scrollable] widgets in a subtree behave.
 ///
 /// Used by [ScrollConfiguration].
-class ScrollConfigurationDelegate {
-  /// Creates a delegate with sensible default behaviors.
+abstract class ScrollConfigurationDelegate {
+  /// Abstract const constructor. This constructor enables subclasses to provide
+  /// const constructors so that they can be used in const expressions.
   const ScrollConfigurationDelegate();
 
+  /// Returns the platform whose scroll physics should be approximated. See
+  /// [ScrollBehavior.platform].
+  TargetPlatform get platform;
+
   /// Returns the ScrollBehavior to be used by generic scrolling containers like
-  /// [Block]. Returns a new [OverscrollWhenScrollableBehavior] by default.
-  ExtentScrollBehavior createScrollBehavior() => new OverscrollWhenScrollableBehavior();
+  /// [Block].
+  ExtentScrollBehavior createScrollBehavior();
 
   /// Generic scrolling containers like [Block] will apply this function to the
   /// Scrollable they create. It can be used to add widgets that wrap the
@@ -24,9 +30,22 @@
   /// [scrollWidget] parameter is returned unchanged.
   Widget wrapScrollWidget(Widget scrollWidget) => scrollWidget;
 
-  /// Overrides should return true if the this ScrollConfigurationDelegate has
-  /// changed in a way that requires rebuilding its scrolling container descendants.
-  /// Returns false by default.
+  /// Overrides should return true if this ScrollConfigurationDelegate differs
+  /// from the provided old delegate in a way that requires rebuilding its
+  /// scrolling container descendants.
+  bool updateShouldNotify(ScrollConfigurationDelegate old);
+}
+
+class _DefaultScrollConfigurationDelegate extends ScrollConfigurationDelegate {
+  const _DefaultScrollConfigurationDelegate();
+
+  @override
+  TargetPlatform get platform => defaultTargetPlatform;
+
+  @override
+  ExtentScrollBehavior createScrollBehavior() => new OverscrollWhenScrollableBehavior(platform: platform);
+
+  @override
   bool updateShouldNotify(ScrollConfigurationDelegate old) => false;
 }
 
@@ -49,7 +68,7 @@
     @required Widget child
   }) : super(key: key, child: child);
 
-  static const ScrollConfigurationDelegate _defaultDelegate = const ScrollConfigurationDelegate();
+  static const ScrollConfigurationDelegate _defaultDelegate = const _DefaultScrollConfigurationDelegate();
 
   /// Defines the ScrollBehavior and scrollable wrapper for descendants.
   final ScrollConfigurationDelegate delegate;
@@ -57,8 +76,10 @@
   /// The delegate property of the closest instance of this class that encloses
   /// the given context.
   ///
-  /// If no such instance exists, returns an instance of the
-  /// [ScrollConfigurationDelegate] base class.
+  /// If no such instance exists, returns a default
+  /// [ScrollConfigurationDelegate] that approximates the scrolling physics of
+  /// the current platform (see [defaultTargetPlatform]) using a
+  /// [OverscrollWhenScrollableBehavior] behavior model.
   static ScrollConfigurationDelegate of(BuildContext context) {
     ScrollConfiguration configuration = context.inheritFromWidgetOfExactType(ScrollConfiguration);
     return configuration?.delegate ?? _defaultDelegate;
@@ -66,7 +87,7 @@
 
   /// A utility function that calls [ScrollConfigurationDelegate.wrapScrollWidget].
   static Widget wrap(BuildContext context, Widget scrollWidget) {
-    return of(context).wrapScrollWidget(scrollWidget);
+    return ScrollConfiguration.of(context).wrapScrollWidget(scrollWidget);
   }
 
   @override
diff --git a/packages/flutter/lib/src/widgets/scrollable.dart b/packages/flutter/lib/src/widgets/scrollable.dart
index b502a49..6ba23b2 100644
--- a/packages/flutter/lib/src/widgets/scrollable.dart
+++ b/packages/flutter/lib/src/widgets/scrollable.dart
@@ -6,8 +6,8 @@
 import 'dart:math' as math;
 import 'dart:ui' as ui show window;
 
-import 'package:flutter/physics.dart';
 import 'package:flutter/gestures.dart';
+import 'package:flutter/physics.dart';
 import 'package:meta/meta.dart';
 
 import 'basic.dart';
diff --git a/packages/flutter/test/widget/lazy_block_fling_test.dart b/packages/flutter/test/widget/lazy_block_fling_test.dart
new file mode 100644
index 0000000..8e1cb3b
--- /dev/null
+++ b/packages/flutter/test/widget/lazy_block_fling_test.dart
@@ -0,0 +1,42 @@
+// Copyright 2015 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:flutter_test/flutter_test.dart';
+import 'package:flutter/material.dart';
+
+const double kHeight = 10.0;
+const double kFlingOffset = kHeight * 20.0;
+
+class TestDelegate extends LazyBlockDelegate {
+  @override
+  Widget buildItem(BuildContext context, int index) {
+    return new Container(height: kHeight);
+  }
+
+  @override
+  double estimateTotalExtent(int firstIndex, int lastIndex, double minOffset, double firstStartOffset, double lastEndOffset) {
+    return double.INFINITY;
+  }
+
+  @override
+  bool shouldRebuild(LazyBlockDelegate oldDelegate) => false;
+}
+
+double currentOffset;
+
+void main() {
+  testWidgets('Flings don\'t stutter', (WidgetTester tester) async {
+    await tester.pumpWidget(new LazyBlock(
+      delegate: new TestDelegate(),
+      onScroll: (double scrollOffset) { currentOffset = scrollOffset; },
+    ));
+    await tester.fling(find.byType(LazyBlock), const Offset(0.0, -kFlingOffset), 1000.0);
+    expect(currentOffset, kFlingOffset);
+    while (tester.binding.transientCallbackCount > 0) {
+      double lastOffset = currentOffset;
+      await tester.pump(const Duration(milliseconds: 20));
+      expect(currentOffset, greaterThan(lastOffset));
+    }
+  }, skip: true); // see https://github.com/flutter/flutter/issues/5339
+}
diff --git a/packages/flutter/test/widget/scroll_behavior_test.dart b/packages/flutter/test/widget/scroll_behavior_test.dart
index 30f65d9..79dd83b 100644
--- a/packages/flutter/test/widget/scroll_behavior_test.dart
+++ b/packages/flutter/test/widget/scroll_behavior_test.dart
@@ -10,7 +10,8 @@
   BoundedBehavior behavior = new BoundedBehavior(
     contentExtent: 150.0,
     containerExtent: 75.0,
-    minScrollOffset: -100.0
+    minScrollOffset: -100.0,
+    platform: TargetPlatform.iOS
   );
     expect(behavior.minScrollOffset, equals(-100.0));
     expect(behavior.maxScrollOffset, equals(-25.0));
diff --git a/packages/flutter/test/widget/scroll_notification_test.dart b/packages/flutter/test/widget/scroll_notification_test.dart
index be1d41f..dd64e93 100644
--- a/packages/flutter/test/widget/scroll_notification_test.dart
+++ b/packages/flutter/test/widget/scroll_notification_test.dart
@@ -6,7 +6,7 @@
 import 'package:flutter/widgets.dart';
 
 void main() {
-  testWidgets('Scroll notifcation basics', (WidgetTester tester) async {
+  testWidgets('Scroll notification basics', (WidgetTester tester) async {
     ScrollNotification notification;
 
     await tester.pumpWidget(new NotificationListener<ScrollNotification>(
@@ -48,7 +48,7 @@
     expect(notification.dragEndDetails.velocity, equals(Velocity.zero));
   });
 
-  testWidgets('Scroll notifcation depth', (WidgetTester tester) async {
+  testWidgets('Scroll notification depth', (WidgetTester tester) async {
     final List<ScrollNotificationKind> depth0Kinds = <ScrollNotificationKind>[];
     final List<ScrollNotificationKind> depth1Kinds = <ScrollNotificationKind>[];
     final List<int> depth0Values = <int>[];
diff --git a/packages/flutter/test/widget/scrollable_fling_test.dart b/packages/flutter/test/widget/scrollable_fling_test.dart
new file mode 100644
index 0000000..108da0b
--- /dev/null
+++ b/packages/flutter/test/widget/scrollable_fling_test.dart
@@ -0,0 +1,61 @@
+// Copyright 2015 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:flutter_test/flutter_test.dart';
+import 'package:flutter/material.dart';
+
+class TestDelegate extends LazyBlockDelegate {
+  @override
+  Widget buildItem(BuildContext context, int index) {
+    return new Text('$index');
+  }
+
+  @override
+  double estimateTotalExtent(int firstIndex, int lastIndex, double minOffset, double firstStartOffset, double lastEndOffset) {
+    return double.INFINITY;
+  }
+
+  @override
+  bool shouldRebuild(LazyBlockDelegate oldDelegate) => false;
+}
+
+double currentOffset;
+
+Future<Null> pumpTest(WidgetTester tester, TargetPlatform platform) async {
+  await tester.pumpWidget(new Container());
+  await tester.pumpWidget(new MaterialApp(
+    theme: new ThemeData(
+      platform: platform
+    ),
+    home: new LazyBlock(
+      delegate: new TestDelegate(),
+      onScroll: (double scrollOffset) { currentOffset = scrollOffset; },
+    ),
+  ));
+  return null;
+}
+
+const double dragOffset = 213.82;
+
+void main() {
+  testWidgets('Flings on different platforms', (WidgetTester tester) async {
+    await pumpTest(tester, TargetPlatform.android);
+    await tester.fling(find.byType(LazyBlock), const Offset(0.0, -dragOffset), 1000.0);
+    expect(currentOffset, dragOffset);
+    await tester.pump(); // trigger fling
+    expect(currentOffset, dragOffset);
+    await tester.pump(const Duration(seconds: 5));
+    final double result1 = currentOffset;
+
+    await pumpTest(tester, TargetPlatform.iOS);
+    await tester.fling(find.byType(LazyBlock), const Offset(0.0, -dragOffset), 1000.0);
+    expect(currentOffset, dragOffset);
+    await tester.pump(); // trigger fling
+    expect(currentOffset, dragOffset);
+    await tester.pump(const Duration(seconds: 5));
+    final double result2 = currentOffset;
+
+    expect(result1, lessThan(result2)); // iOS (result2) is slipperier than Android (result1)
+  });
+}
diff --git a/packages/flutter_test/lib/src/controller.dart b/packages/flutter_test/lib/src/controller.dart
index 7899c44..2cd215b 100644
--- a/packages/flutter_test/lib/src/controller.dart
+++ b/packages/flutter_test/lib/src/controller.dart
@@ -264,13 +264,29 @@
   ///
   /// If the middle of the widget is not exposed, this might send
   /// events to another object.
-  Future<Null> fling(Finder finder, Offset offset, double velocity, { int pointer: 1 }) {
-    return flingFrom(getCenter(finder), offset, velocity, pointer: pointer);
+  ///
+  /// This can pump frames. See [flingFrom] for a discussion of how the
+  /// `offset`, `velocity` and `frameInterval` arguments affect this.
+  Future<Null> fling(Finder finder, Offset offset, double velocity, { int pointer: 1, Duration frameInterval: const Duration(milliseconds: 16) }) {
+    return flingFrom(getCenter(finder), offset, velocity, pointer: pointer, frameInterval: frameInterval);
   }
 
   /// Attempts a fling gesture starting from the given location,
   /// moving the given distance, reaching the given velocity.
-  Future<Null> flingFrom(Point startLocation, Offset offset, double velocity, { int pointer: 1 }) {
+  ///
+  /// Exactly 50 pointer events are synthesized.
+  ///
+  /// The offset and velocity control the interval between each pointer event.
+  /// For example, if the offset is 200 pixels, and the velocity is 800 pixels
+  /// per second, the pointer events will be sent for each increment of 4 pixels
+  /// (200/50), over 250ms (200/800), meaning events will be sent every 1.25ms
+  /// (250/200).
+  ///
+  /// To make tests more realistic, frames may be pumped during this time (using
+  /// calls to [pump]). If the total duration is longer than `frameInterval`,
+  /// then one frame is pumped each time that amount of time elapses while
+  /// sending events, or each time an event is synthesised, whichever is rarer.
+  Future<Null> flingFrom(Point startLocation, Offset offset, double velocity, { int pointer: 1, Duration frameInterval: const Duration(milliseconds: 16) }) {
     return TestAsyncUtils.guard(() async {
       assert(offset.distance > 0.0);
       assert(velocity != 0.0);   // velocity is pixels/second
@@ -279,17 +295,32 @@
       const int kMoveCount = 50; // Needs to be >= kHistorySize, see _LeastSquaresVelocityTrackerStrategy
       final double timeStampDelta = 1000.0 * offset.distance / (kMoveCount * velocity);
       double timeStamp = 0.0;
+      double lastTimeStamp = timeStamp;
       await sendEventToBinding(p.down(startLocation, timeStamp: new Duration(milliseconds: timeStamp.round())), result);
-      for (int i = 0; i <= kMoveCount; i++) {
+      for (int i = 0; i <= kMoveCount; i += 1) {
         final Point location = startLocation + Offset.lerp(Offset.zero, offset, i / kMoveCount);
         await sendEventToBinding(p.move(location, timeStamp: new Duration(milliseconds: timeStamp.round())), result);
         timeStamp += timeStampDelta;
+        if (timeStamp - lastTimeStamp > frameInterval.inMilliseconds) {
+          await pump(new Duration(milliseconds: (timeStamp - lastTimeStamp).truncate()));
+          lastTimeStamp = timeStamp;
+        }
       }
       await sendEventToBinding(p.up(timeStamp: new Duration(milliseconds: timeStamp.round())), result);
       return null;
     });
   }
 
+  /// Called to indicate that time should advance.
+  ///
+  /// This is invoked by [flingFrom], for instance, so that the sequence of
+  /// pointer events occurs over time.
+  ///
+  /// The default implementation does nothing.
+  ///
+  /// The [WidgetTester] subclass implements this by deferring to the [binding].
+  Future<Null> pump(Duration duration) => new Future<Null>.value(null);
+
   /// Attempts to drag the given widget by the given offset, by
   /// starting a drag in the middle of the widget.
   ///
diff --git a/packages/flutter_test/lib/src/test_async_utils.dart b/packages/flutter_test/lib/src/test_async_utils.dart
index 6aad085..e0726be 100644
--- a/packages/flutter_test/lib/src/test_async_utils.dart
+++ b/packages/flutter_test/lib/src/test_async_utils.dart
@@ -67,7 +67,7 @@
     _AsyncScope scope = new _AsyncScope(StackTrace.current, zone);
     _scopeStack.add(scope);
     Future<Null> result = scope.zone.run(body);
-    result = result.whenComplete(() {
+    void completionHandler(dynamic error, StackTrace stack) {
       assert(_scopeStack.isNotEmpty);
       assert(_scopeStack.contains(scope));
       bool leaked = false;
@@ -77,8 +77,10 @@
         closedScope = _scopeStack.removeLast();
         if (closedScope == scope)
           break;
-        leaked = true;
-        message.writeln('Asynchronous call to guarded function leaked. You must use "await" with all Future-returning test APIs.');
+        if (!leaked) {
+          message.writeln('Asynchronous call to guarded function leaked. You must use "await" with all Future-returning test APIs.');
+          leaked = true;
+        }
         final _StackEntry originalGuarder = _findResponsibleMethod(closedScope.creationStack, 'guard', message);
         if (originalGuarder != null) {
           message.writeln(
@@ -90,10 +92,20 @@
           );
         }
       }
-      if (leaked)
+      if (leaked) {
+        if (error != null) {
+          message.writeln('An uncaught exception may have caused the guarded function leak. The exception was:');
+          message.writeln('$error');
+          message.writeln('The stack trace associated with this exception was:');
+          FlutterError.defaultStackFilter(stack.toString().trimRight().split('\n')).forEach(message.writeln);
+        }
         throw new FlutterError(message.toString().trimRight());
-    });
-    return result;
+      }
+    }
+    return result.then(
+      (Null value) => completionHandler(null, null),
+      onError: completionHandler
+    );
   }
 
   static Zone get _currentScopeZone {
diff --git a/packages/flutter_test/lib/src/widget_tester.dart b/packages/flutter_test/lib/src/widget_tester.dart
index 06fdbe5..8b96c59 100644
--- a/packages/flutter_test/lib/src/widget_tester.dart
+++ b/packages/flutter_test/lib/src/widget_tester.dart
@@ -36,9 +36,9 @@
 ///
 /// Example:
 ///
-///     testWidgets('MyWidget', (WidgetTester tester) {
-///       tester.pumpWidget(new MyWidget());
-///       tester.tap(find.text('Save'));
+///     testWidgets('MyWidget', (WidgetTester tester) async {
+///       await tester.pumpWidget(new MyWidget());
+///       await tester.tap(find.text('Save'));
 ///       expect(tester, hasWidget(find.text('Success')));
 ///     });
 void testWidgets(String description, WidgetTesterCallback callback, {
@@ -77,12 +77,12 @@
 ///
 ///     main() async {
 ///       assert(false); // fail in checked mode
-///       await benchmarkWidgets((WidgetTester tester) {
-///         tester.pumpWidget(new MyWidget());
+///       await benchmarkWidgets((WidgetTester tester) async {
+///         await tester.pumpWidget(new MyWidget());
 ///         final Stopwatch timer = new Stopwatch()..start();
 ///         for (int index = 0; index < 10000; index += 1) {
-///           tester.tap(find.text('Tap me'));
-///           tester.pump();
+///           await tester.tap(find.text('Tap me'));
+///           await tester.pump();
 ///         }
 ///         timer.stop();
 ///         debugPrint('Time taken: ${timer.elapsedMilliseconds}ms');
@@ -99,7 +99,7 @@
     print('│  enabled will not accurately reflect the performance  │');
     print('│  that will be experienced by end users using release  ╎');
     print('│  builds. Benchmarks should be run using this command  ┆');
-    print('│  line:  flutter run --release -t benchmark.dart       ┊');
+    print('│  line:  flutter run --release benchmark.dart          ┊');
     print('│                                                        ');
     print('└─────────────────────────────────────────────────╌┄┈  🐢');
     return true;
@@ -172,6 +172,7 @@
   ///
   /// This is a convenience function that just calls
   /// [TestWidgetsFlutterBinding.pump].
+  @override
   Future<Null> pump([
     Duration duration,
     EnginePhase phase = EnginePhase.sendSemanticsTree