From aa8f86135de71b3602952159f70696cd3020f031 Mon Sep 17 00:00:00 2001 From: Michael Goderbauer Date: Tue, 12 Feb 2019 18:00:35 -0800 Subject: [PATCH] Fix crash when disposing nested Scrollables while holding in overscroll position (#27864) --- .../src/widgets/notification_listener.dart | 8 ++- .../test/widgets/scrollable_dispose_test.dart | 66 +++++++++++++++++++ 2 files changed, 71 insertions(+), 3 deletions(-) diff --git a/packages/flutter/lib/src/widgets/notification_listener.dart b/packages/flutter/lib/src/widgets/notification_listener.dart index f1be92ae720..9d77b3b81a4 100644 --- a/packages/flutter/lib/src/widgets/notification_listener.dart +++ b/packages/flutter/lib/src/widgets/notification_listener.dart @@ -53,10 +53,12 @@ abstract class Notification { /// /// The notification will be delivered to any [NotificationListener] widgets /// with the appropriate type parameters that are ancestors of the given - /// [BuildContext]. + /// [BuildContext]. If the [BuildContext] is null, the notification is not + /// dispatched. void dispatch(BuildContext target) { - assert(target != null); // Only call dispatch if the widget's State is still mounted. - target.visitAncestorElements(visitAncestor); + // The `target` may be null if the subtree the notification is supposed to be + // dispatched in is in the process of being disposed. + target?.visitAncestorElements(visitAncestor); } @override diff --git a/packages/flutter/test/widgets/scrollable_dispose_test.dart b/packages/flutter/test/widgets/scrollable_dispose_test.dart index a55d47496db..f8dc25781a5 100644 --- a/packages/flutter/test/widgets/scrollable_dispose_test.dart +++ b/packages/flutter/test/widgets/scrollable_dispose_test.dart @@ -2,6 +2,8 @@ // 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/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:flutter/widgets.dart'; @@ -28,4 +30,68 @@ void main() { tester.state(find.byType(FlipWidget)).flip(); await tester.pump(const Duration(hours: 5)); }); + + testWidgets('Disposing a (nested) Scrollable while holding in overscroll (iOS) does not crash', (WidgetTester tester) async { + // Regression test for https://github.com/flutter/flutter/issues/27707. + + debugDefaultTargetPlatformOverride = TargetPlatform.iOS; + final ScrollController controller = ScrollController(); + final Key outterContainer = GlobalKey(); + + await tester.pumpWidget( + MaterialApp( + home: Center( + child: Container( + key: outterContainer, + color: Colors.purple, + width: 400.0, + child: SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Container( + width: 500.0, + child: ListView.builder( + controller: controller, + itemBuilder: (BuildContext context, int index) { + return Container( + color: index % 2 == 0 ? Colors.red : Colors.green, + height: 200.0, + child: Text('Hello $index'), + ); + }, + ), + ), + ), + ), + ), + ), + ); + + // Go into overscroll. + double lastScrollOffset; + await tester.fling(find.text('Hello 0'), const Offset(0.0, 1000.0), 1000.0); + await tester.pump(const Duration(milliseconds: 100)); + expect(lastScrollOffset = controller.offset, lessThan(0.0)); + + // Reduce the overscroll a little, but don't let it go back to 0.0. + await tester.pump(const Duration(milliseconds: 100)); + expect(controller.offset, greaterThan(lastScrollOffset)); + expect(controller.offset, lessThan(0.0)); + final double currentOffset = controller.offset; + + // Start a hold activity by putting one pointer down. + await tester.startGesture(tester.getTopLeft(find.byKey(outterContainer)) + const Offset(50.0, 50.0)); + await tester.pumpAndSettle(); // This shouldn't change the scroll offset because of the down event above. + expect(controller.offset, currentOffset); + + // Dispose the scrollables while the finger is still down, this should not crash. + await tester.pumpWidget( + MaterialApp( + home: Container(), + ) + ); + await tester.pumpAndSettle(); + expect(controller.hasClients, isFalse); + + debugDefaultTargetPlatformOverride = null; + }); }