Unverified Commit 57d714eb authored by chunhtai's avatar chunhtai Committed by GitHub

fix widget built twice during warm up frame (#39079)

parent 296e97f3
......@@ -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();
}
......
......@@ -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;
}
}
......
// 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 <Widget>[
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;
}
// 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);
});
});
}
......@@ -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<void> idle() {
final Future<void> result = super.idle();
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment