diff --git a/packages/flutter/lib/src/widgets/binding.dart b/packages/flutter/lib/src/widgets/binding.dart index 8e708af423..0f4518300b 100644 --- a/packages/flutter/lib/src/widgets/binding.dart +++ b/packages/flutter/lib/src/widgets/binding.dart @@ -794,6 +794,17 @@ mixin WidgetsBinding on BindingBase, SchedulerBinding, GestureBinding, RendererB Element get renderViewElement => _renderViewElement; Element _renderViewElement; + /// Schedules a [Timer] for attaching the root widget. + /// + /// This is called by [runApp] to configure the widget tree. Consider using + /// [attachRootWidget] if you want to build the widget tree synchronously. + @protected + void scheduleAttachRootWidget(Widget rootWidget) { + Timer.run(() { + attachRootWidget(rootWidget); + }); + } + /// Takes a widget and attaches it to the [renderViewElement], creating it if /// necessary. /// @@ -855,7 +866,7 @@ mixin WidgetsBinding on BindingBase, SchedulerBinding, GestureBinding, RendererB /// ensure the widget, element, and render trees are all built. void runApp(Widget app) { WidgetsFlutterBinding.ensureInitialized() - ..attachRootWidget(app) + ..scheduleAttachRootWidget(app) ..scheduleWarmUpFrame(); } diff --git a/packages/flutter/lib/src/widgets/nested_scroll_view.dart b/packages/flutter/lib/src/widgets/nested_scroll_view.dart index b0a0c7bf1d..5b56c8d812 100644 --- a/packages/flutter/lib/src/widgets/nested_scroll_view.dart +++ b/packages/flutter/lib/src/widgets/nested_scroll_view.dart @@ -498,13 +498,8 @@ class _NestedScrollCoordinator implements ScrollActivityDelegate, ScrollHoldCont bool get hasScrolledBody { for (_NestedScrollPosition position in _innerPositions) { - // TODO(chunhtai): Replace null check with assert once - // https://github.com/flutter/flutter/issues/31195 is fixed. - if ( - position.minScrollExtent != null && - position.pixels != null && - position.pixels > position.minScrollExtent - ) { + assert(position.minScrollExtent != null && position.pixels != null); + if (position.pixels > position.minScrollExtent) { return true; } } diff --git a/packages/flutter/test/widgets/nested_scroll_view_async_test.dart b/packages/flutter/test/widgets/nested_scroll_view_async_test.dart deleted file mode 100644 index 075fc133bb..0000000000 --- a/packages/flutter/test/widgets/nested_scroll_view_async_test.dart +++ /dev/null @@ -1,72 +0,0 @@ -// Copyright 2019 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/material.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:flutter/gestures.dart' show DragStartBehavior; -import 'package:quiver/testing/async.dart'; - -void main() { - setUp(() { - WidgetsFlutterBinding.ensureInitialized(); - WidgetsBinding.instance.resetEpoch(); - }); - - test('NestedScrollView can build sccessfully if mark dirty during warm up frame', () { - final FakeAsync fakeAsync = FakeAsync(); - fakeAsync.run((FakeAsync async) { - runApp( - MaterialApp( - home: Material( - child: DefaultTabController( - length: 1, - child: NestedScrollView( - dragStartBehavior: DragStartBehavior.down, - headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) { - return [ - const SliverPersistentHeader( - delegate: TestHeader(), - ), - ]; - }, - body: SingleChildScrollView( - dragStartBehavior: DragStartBehavior.down, - child: Container( - height: 1000.0, - child: const Placeholder(), - ), - ), - ), - ), - ), - ), - ); - // Marks element as dirty right before the first draw frame is called. - // This can happen when engine flush user setting. - final Element element = find.byType(NestedScrollView, skipOffstage: false).evaluate().single; - element.markNeedsBuild(); - // Triggers draw frame timer scheduled in scheduleWarmUpFrame. - fakeAsync.flushTimers(); - }); - // Make sure widget is rebuilt correctly. - expect( - find.byType(NestedScrollView, skipOffstage: false).evaluate().single.widget is NestedScrollView, - isTrue - ); - }); -} - -class TestHeader extends SliverPersistentHeaderDelegate { - const TestHeader(); - @override - double get minExtent => 100.0; - @override - double get maxExtent => 100.0; - @override - Widget build(BuildContext context, double shrinkOffset, bool overlapsContent) { - return const Placeholder(); - } - @override - bool shouldRebuild(TestHeader oldDelegate) => false; -} diff --git a/packages/flutter/test/widgets/run_app_async_test.dart b/packages/flutter/test/widgets/run_app_async_test.dart new file mode 100644 index 0000000000..8645b2ec68 --- /dev/null +++ b/packages/flutter/test/widgets/run_app_async_test.dart @@ -0,0 +1,31 @@ +// Copyright 2016 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/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:quiver/testing/async.dart'; + +void main() { + setUp(() { + WidgetsFlutterBinding.ensureInitialized(); + WidgetsBinding.instance.resetEpoch(); + }); + + test('WidgetBinding build rendering tree and warm up frame back to back', () { + final FakeAsync fakeAsync = FakeAsync(); + fakeAsync.run((FakeAsync async) { + runApp( + const MaterialApp( + home: Material( + child: Text('test'), + ), + ), + ); + // Rendering tree is not built synchronously. + expect(WidgetsBinding.instance.renderViewElement, isNull); + fakeAsync.flushTimers(); + expect(WidgetsBinding.instance.renderViewElement, isNotNull); + }); + }); +} diff --git a/packages/flutter_test/lib/src/binding.dart b/packages/flutter_test/lib/src/binding.dart index 56601416e8..71b7650d53 100644 --- a/packages/flutter_test/lib/src/binding.dart +++ b/packages/flutter_test/lib/src/binding.dart @@ -993,6 +993,14 @@ class AutomatedTestWidgetsFlutterBinding extends TestWidgetsFlutterBinding { _currentFakeAsync.flushMicrotasks(); } + @override + void scheduleAttachRootWidget(Widget rootWidget) { + // We override the default version of this so that the application-startup widget tree + // build does not schedule timers which we might never get around to running. + attachRootWidget(rootWidget); + _currentFakeAsync.flushMicrotasks(); + } + @override Future idle() { final Future result = super.idle();