diff --git a/packages/flutter/lib/src/rendering/binding.dart b/packages/flutter/lib/src/rendering/binding.dart index 4b4343a1f0..0ad6a7a610 100644 --- a/packages/flutter/lib/src/rendering/binding.dart +++ b/packages/flutter/lib/src/rendering/binding.dart @@ -284,6 +284,62 @@ mixin RendererBinding on BindingBase, ServicesBinding, SchedulerBinding, Gesture _mouseTracker.schedulePostFrameCheck(); } + int _firstFrameDeferredCount = 0; + bool _firstFrameSent = false; + + /// Whether frames produced by [drawFrame] are sent to the engine. + /// + /// If false the framework will do all the work to produce a frame, + /// but the frame is never send to the engine to actually appear on screen. + /// + /// See also: + /// + /// * [deferFirstFrame], which defers when the first frame is send to the + /// engine. + bool get sendFramesToEngine => _firstFrameSent || _firstFrameDeferredCount == 0; + + /// Tell the framework to not send the first frames to the engine until there + /// is a corresponding call to [allowFirstFrame]. + /// + /// Call this to perform asynchronous initialisation work before the first + /// frame is rendered (which takes down the splash screen). The framework + /// will still do all the work to produce frames, but those frames are never + /// send to the engine and will not appear on screen. + /// + /// Calling this has no effect after the first frame has been send to the + /// engine. + void deferFirstFrame() { + assert(_firstFrameDeferredCount >= 0); + _firstFrameDeferredCount += 1; + } + + /// Called after [deferFirstFrame] to tell the framework that it is ok to + /// send the first frame to the engine now. + /// + /// For best performance, this method should only be called while the + /// [schedulerPhase] is [SchedulerPhase.idle]. + /// + /// This method may only be called once for each corresponding call + /// to [deferFirstFrame]. + void allowFirstFrame() { + assert(_firstFrameDeferredCount > 0); + _firstFrameDeferredCount -= 1; + // Always schedule a warm up frame even if the deferral count is not down to + // zero yet since the removal of a deferral may uncover new deferrals that + // are lower in the widget tree. + if (!_firstFrameSent) + scheduleWarmUpFrame(); + } + + /// Call this to pretend that no frames have been sent to the engine yet. + /// + /// This is useful for tests that want to call [deferFirstFrame] and + /// [allowFirstFrame] since those methods only have an effect if no frames + /// have been sent to the engine yet. + void resetFirstFrameSent() { + _firstFrameSent = false; + } + /// Pump the rendering pipeline to generate a frame. /// /// This method is called by [handleDrawFrame], which itself is called @@ -345,8 +401,11 @@ mixin RendererBinding on BindingBase, ServicesBinding, SchedulerBinding, Gesture pipelineOwner.flushLayout(); pipelineOwner.flushCompositingBits(); pipelineOwner.flushPaint(); - renderView.compositeFrame(); // this sends the bits to the GPU - pipelineOwner.flushSemantics(); // this also sends the semantics to the OS. + if (sendFramesToEngine) { + renderView.compositeFrame(); // this sends the bits to the GPU + pipelineOwner.flushSemantics(); // this also sends the semantics to the OS. + _firstFrameSent = true; + } } @override diff --git a/packages/flutter/lib/src/widgets/binding.dart b/packages/flutter/lib/src/widgets/binding.dart index 8cae6c137b..80f1b14ed9 100644 --- a/packages/flutter/lib/src/widgets/binding.dart +++ b/packages/flutter/lib/src/widgets/binding.dart @@ -574,9 +574,6 @@ mixin WidgetsBinding on BindingBase, ServicesBinding, SchedulerBinding, GestureB } bool _needToReportFirstFrame = true; - int _deferFirstFrameReportCount = 0; - bool get _reportFirstFrame => _deferFirstFrameReportCount == 0; - final Completer _firstFrameCompleter = Completer(); @@ -602,11 +599,6 @@ mixin WidgetsBinding on BindingBase, ServicesBinding, SchedulerBinding, GestureB /// Whether the first frame has finished building. /// - /// Only useful in profile and debug builds; in release builds, this always - /// return false. This can be deferred using [deferFirstFrameReport] and - /// [allowFirstFrameReport]. The value is set at the end of the call to - /// [drawFrame]. - /// /// This value can also be obtained over the VM service protocol as /// `ext.flutter.didSendFirstFrameEvent`. /// @@ -618,27 +610,30 @@ mixin WidgetsBinding on BindingBase, ServicesBinding, SchedulerBinding, GestureB /// Tell the framework not to report the frame it is building as a "useful" /// first frame until there is a corresponding call to [allowFirstFrameReport]. /// - /// This is used by [WidgetsApp] to avoid reporting frames that aren't useful - /// during startup as the "first frame". + /// Deprecated. Use [deferFirstFrame]/[allowFirstFrame] to delay rendering the + /// first frame. + @Deprecated( + 'Use deferFirstFrame/allowFirstFrame to delay rendering the first frame. ' + 'This feature was deprecated after v1.12.4.' + ) void deferFirstFrameReport() { if (!kReleaseMode) { - assert(_deferFirstFrameReportCount >= 0); - _deferFirstFrameReportCount += 1; + deferFirstFrame(); } } /// When called after [deferFirstFrameReport]: tell the framework to report /// the frame it is building as a "useful" first frame. /// - /// This method may only be called once for each corresponding call - /// to [deferFirstFrameReport]. - /// - /// This is used by [WidgetsApp] to report when the first useful frame is - /// painted. + /// Deprecated. Use [deferFirstFrame]/[allowFirstFrame] to delay rendering the + /// first frame. + @Deprecated( + 'Use deferFirstFrame/allowFirstFrame to delay rendering the first frame. ' + 'This feature was deprecated after v1.12.4.' + ) void allowFirstFrameReport() { if (!kReleaseMode) { - assert(_deferFirstFrameReportCount >= 1); - _deferFirstFrameReportCount -= 1; + allowFirstFrame(); } } @@ -755,18 +750,23 @@ mixin WidgetsBinding on BindingBase, ServicesBinding, SchedulerBinding, GestureB return true; }()); - if (_needToReportFirstFrame && _reportFirstFrame) { + TimingsCallback firstFrameCallback; + if (_needToReportFirstFrame) { assert(!_firstFrameCompleter.isCompleted); - TimingsCallback firstFrameCallback; firstFrameCallback = (List timings) { + assert(sendFramesToEngine); if (!kReleaseMode) { developer.Timeline.instantSync('Rasterized first useful frame'); developer.postEvent('Flutter.FirstFrame', {}); } SchedulerBinding.instance.removeTimingsCallback(firstFrameCallback); + firstFrameCallback = null; _firstFrameCompleter.complete(); }; + // Callback is only invoked when [Window.render] is called. When + // [sendFramesToEngine] is set to false during the frame, it will not + // be called and we need to remove the callback (see below). SchedulerBinding.instance.addTimingsCallback(firstFrameCallback); } @@ -782,11 +782,14 @@ mixin WidgetsBinding on BindingBase, ServicesBinding, SchedulerBinding, GestureB }()); } if (!kReleaseMode) { - if (_needToReportFirstFrame && _reportFirstFrame) { + if (_needToReportFirstFrame && sendFramesToEngine) { developer.Timeline.instantSync('Widgets built first useful frame'); } } _needToReportFirstFrame = false; + if (firstFrameCallback != null && !sendFramesToEngine) { + SchedulerBinding.instance.removeTimingsCallback(firstFrameCallback); + } } /// The [Element] that is at the root of the hierarchy (and which wraps the @@ -834,12 +837,9 @@ mixin WidgetsBinding on BindingBase, ServicesBinding, SchedulerBinding, GestureB return true; }()); - deferFirstFrameReport(); if (renderViewElement != null) buildOwner.reassemble(renderViewElement); - return super.performReassemble().then((void value) { - allowFirstFrameReport(); - }); + return super.performReassemble(); } } diff --git a/packages/flutter/lib/src/widgets/localizations.dart b/packages/flutter/lib/src/widgets/localizations.dart index 775fb46eb9..2161222dbe 100644 --- a/packages/flutter/lib/src/widgets/localizations.dart +++ b/packages/flutter/lib/src/widgets/localizations.dart @@ -6,6 +6,7 @@ import 'dart:async'; import 'dart:ui' show Locale; import 'package:flutter/foundation.dart'; +import 'package:flutter/rendering.dart'; import 'basic.dart'; import 'binding.dart'; @@ -519,15 +520,15 @@ class _LocalizationsState extends State { // have finished loading. Until then the old locale will continue to be used. // - If we're running at app startup time then defer reporting the first // "useful" frame until after the async load has completed. - WidgetsBinding.instance.deferFirstFrameReport(); + RendererBinding.instance.deferFirstFrame(); typeToResourcesFuture.then((Map value) { - WidgetsBinding.instance.allowFirstFrameReport(); - if (!mounted) - return; - setState(() { - _typeToResources = value; - _locale = locale; - }); + if (mounted) { + setState(() { + _typeToResources = value; + _locale = locale; + }); + } + RendererBinding.instance.allowFirstFrame(); }); } } diff --git a/packages/flutter/test/widgets/binding_deferred_first_frame_test.dart b/packages/flutter/test/widgets/binding_deferred_first_frame_test.dart new file mode 100644 index 0000000000..27e3784581 --- /dev/null +++ b/packages/flutter/test/widgets/binding_deferred_first_frame_test.dart @@ -0,0 +1,120 @@ +// 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 'dart:async'; + +import 'package:flutter/rendering.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; + +const String _actualContent = 'Actual Content'; +const String _loading = 'Loading...'; + +void main() { + testWidgets('deferFirstFrame/allowFirstFrame stops sending frames to engine', (WidgetTester tester) async { + expect(RendererBinding.instance.sendFramesToEngine, isTrue); + + final Completer completer = Completer(); + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: _DeferringWidget( + key: UniqueKey(), + loader: completer.future, + ), + ), + ); + final _DeferringWidgetState state = tester.state<_DeferringWidgetState>(find.byType(_DeferringWidget)); + + expect(find.text(_loading), findsOneWidget); + expect(find.text(_actualContent), findsNothing); + expect(RendererBinding.instance.sendFramesToEngine, isFalse); + + await tester.pump(); + expect(find.text(_loading), findsOneWidget); + expect(find.text(_actualContent), findsNothing); + expect(RendererBinding.instance.sendFramesToEngine, isFalse); + expect(state.doneLoading, isFalse); + + // Complete the future to start sending frames. + completer.complete(); + await tester.idle(); + expect(state.doneLoading, isTrue); + expect(RendererBinding.instance.sendFramesToEngine, isTrue); + + await tester.pump(); + expect(find.text(_loading), findsNothing); + expect(find.text(_actualContent), findsOneWidget); + expect(RendererBinding.instance.sendFramesToEngine, isTrue); + }); + + testWidgets('Two widgets can defer frames', (WidgetTester tester) async { + expect(RendererBinding.instance.sendFramesToEngine, isTrue); + + final Completer completer1 = Completer(); + final Completer completer2 = Completer(); + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: Row( + children: [ + _DeferringWidget( + key: UniqueKey(), + loader: completer1.future, + ), + _DeferringWidget( + key: UniqueKey(), + loader: completer2.future, + ), + ], + ), + ), + ); + expect(find.text(_loading), findsNWidgets(2)); + expect(find.text(_actualContent), findsNothing); + expect(RendererBinding.instance.sendFramesToEngine, isFalse); + + completer1.complete(); + completer2.complete(); + await tester.idle(); + + await tester.pump(); + expect(find.text(_loading), findsNothing); + expect(find.text(_actualContent), findsNWidgets(2)); + expect(RendererBinding.instance.sendFramesToEngine, isTrue); + }); +} + +class _DeferringWidget extends StatefulWidget { + const _DeferringWidget({Key key, this.loader}) : super(key: key); + + final Future loader; + + @override + State<_DeferringWidget> createState() => _DeferringWidgetState(); +} + +class _DeferringWidgetState extends State<_DeferringWidget> { + bool doneLoading = false; + + @override + void initState() { + super.initState(); + RendererBinding.instance.deferFirstFrame(); + widget.loader.then((_) { + setState(() { + doneLoading = true; + RendererBinding.instance.allowFirstFrame(); + }); + }); + } + + @override + Widget build(BuildContext context) { + return doneLoading + ? const Text(_actualContent) + : const Text(_loading); + } +} diff --git a/packages/flutter/test/widgets/localizations_test.dart b/packages/flutter/test/widgets/localizations_test.dart new file mode 100644 index 0000000000..535eb6fa19 --- /dev/null +++ b/packages/flutter/test/widgets/localizations_test.dart @@ -0,0 +1,73 @@ +// Copyright 2013 The Flutter 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 'dart:async'; + +import 'package:flutter_test/flutter_test.dart'; +import 'package:flutter/widgets.dart'; + +void main() { + final TestAutomatedTestWidgetsFlutterBinding binding = TestAutomatedTestWidgetsFlutterBinding(); + + testWidgets('Locale is available when Localizations widget stops defering frames', (WidgetTester tester) async { + final FakeLocalizationsDelegate delegate = FakeLocalizationsDelegate(); + await tester.pumpWidget(Localizations( + locale: const Locale('fo'), + delegates: >[ + WidgetsLocalizationsDelegate(), + delegate, + ], + child: const Text('loaded') + )); + final dynamic state = tester.state(find.byType(Localizations)); + expect(state.locale, isNull); + expect(find.text('loaded'), findsNothing); + + Locale locale; + binding.onAllowFrame = () { + locale = state.locale; + }; + delegate.completer.complete('foo'); + await tester.idle(); + expect(locale, const Locale('fo')); + await tester.pump(); + expect(find.text('loaded'), findsOneWidget); + }); +} + +class FakeLocalizationsDelegate extends LocalizationsDelegate { + final Completer completer = Completer(); + + @override + bool isSupported(Locale locale) => true; + + @override + Future load(Locale locale) => completer.future; + + @override + bool shouldReload(LocalizationsDelegate old) => false; +} + +class TestAutomatedTestWidgetsFlutterBinding extends AutomatedTestWidgetsFlutterBinding { + + VoidCallback onAllowFrame; + + @override + void allowFirstFrame() { + if (onAllowFrame != null) + onAllowFrame(); + super.allowFirstFrame(); + } +} + +class WidgetsLocalizationsDelegate extends LocalizationsDelegate { + @override + bool isSupported(Locale locale) => true; + + @override + Future load(Locale locale) => DefaultWidgetsLocalizations.load(locale); + + @override + bool shouldReload(WidgetsLocalizationsDelegate old) => false; +} diff --git a/packages/flutter_test/lib/src/binding.dart b/packages/flutter_test/lib/src/binding.dart index 8b78b6831a..32c91c085e 100644 --- a/packages/flutter_test/lib/src/binding.dart +++ b/packages/flutter_test/lib/src/binding.dart @@ -687,6 +687,9 @@ abstract class TestWidgetsFlutterBinding extends BindingBase runApp(Container(key: UniqueKey(), child: _preTestMessage)); // Reset the tree to a known state. await pump(); + // Pretend that the first frame produced in the test body is the first frame + // sent to the engine. + resetFirstFrameSent(); final bool autoUpdateGoldensBeforeTest = autoUpdateGoldenFiles && !isBrowser; final TestExceptionReporter reportTestExceptionBeforeTest = reportTestException; @@ -963,6 +966,31 @@ class AutomatedTestWidgetsFlutterBinding extends TestWidgetsFlutterBinding { return result; } + int _firstFrameDeferredCount = 0; + bool _firstFrameSent = false; + + @override + bool get sendFramesToEngine => _firstFrameSent || _firstFrameDeferredCount == 0; + + @override + void deferFirstFrame() { + assert(_firstFrameDeferredCount >= 0); + _firstFrameDeferredCount += 1; + } + + @override + void allowFirstFrame() { + assert(_firstFrameDeferredCount > 0); + _firstFrameDeferredCount -= 1; + // Unlike in RendererBinding.allowFirstFrame we do not force a frame her + // to give the test full control over frame scheduling. + } + + @override + void resetFirstFrameSent() { + _firstFrameSent = false; + } + EnginePhase _phase = EnginePhase.sendSemanticsUpdate; // Cloned from RendererBinding.drawFrame() but with early-exit semantics. @@ -979,7 +1007,8 @@ class AutomatedTestWidgetsFlutterBinding extends TestWidgetsFlutterBinding { pipelineOwner.flushCompositingBits(); if (_phase != EnginePhase.compositingBits) { pipelineOwner.flushPaint(); - if (_phase != EnginePhase.paint) { + if (_phase != EnginePhase.paint && sendFramesToEngine) { + _firstFrameSent = true; renderView.compositeFrame(); // this sends the bits to the GPU if (_phase != EnginePhase.composite) { pipelineOwner.flushSemantics();