Add option to delay rendering the first frame (#45135)

diff --git a/packages/flutter/lib/src/rendering/binding.dart b/packages/flutter/lib/src/rendering/binding.dart
index adf1037..9cfb575 100644
--- a/packages/flutter/lib/src/rendering/binding.dart
+++ b/packages/flutter/lib/src/rendering/binding.dart
@@ -283,6 +283,53 @@
     drawFrame();
   }
 
+  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();
+  }
+
   /// Pump the rendering pipeline to generate a frame.
   ///
   /// This method is called by [handleDrawFrame], which itself is called
@@ -344,8 +391,11 @@
     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 c4eb958..1fd7d81 100644
--- a/packages/flutter/lib/src/widgets/binding.dart
+++ b/packages/flutter/lib/src/widgets/binding.dart
@@ -572,9 +572,6 @@
   }
 
   bool _needToReportFirstFrame = true;
-  int _deferFirstFrameReportCount = 0;
-  bool get _reportFirstFrame => _deferFirstFrameReportCount == 0;
-
 
   final Completer<void> _firstFrameCompleter = Completer<void>();
 
@@ -600,11 +597,6 @@
 
   /// 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`.
   ///
@@ -616,27 +608,30 @@
   /// 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();
     }
   }
 
@@ -753,18 +748,23 @@
       return true;
     }());
 
-    if (_needToReportFirstFrame && _reportFirstFrame) {
+    TimingsCallback firstFrameCallback;
+    if (_needToReportFirstFrame) {
       assert(!_firstFrameCompleter.isCompleted);
 
-      TimingsCallback firstFrameCallback;
       firstFrameCallback = (List<FrameTiming> timings) {
+        assert(sendFramesToEngine);
         if (!kReleaseMode) {
           developer.Timeline.instantSync('Rasterized first useful frame');
           developer.postEvent('Flutter.FirstFrame', <String, dynamic>{});
         }
         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);
     }
 
@@ -780,11 +780,14 @@
       }());
     }
     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
@@ -832,12 +835,9 @@
       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 0cc93a8..29196b3 100644
--- a/packages/flutter/lib/src/widgets/localizations.dart
+++ b/packages/flutter/lib/src/widgets/localizations.dart
@@ -6,6 +6,7 @@
 import 'dart:ui' show Locale;
 
 import 'package:flutter/foundation.dart';
+import 'package:flutter/rendering.dart';
 
 import 'basic.dart';
 import 'binding.dart';
@@ -519,9 +520,9 @@
       // 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<void>((Map<Type, dynamic> value) {
-        WidgetsBinding.instance.allowFirstFrameReport();
+        RendererBinding.instance.allowFirstFrame();
         if (!mounted)
           return;
         setState(() {
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 0000000..0f6d019
--- /dev/null
+++ b/packages/flutter/test/widgets/binding_deferred_first_frame_test.dart
@@ -0,0 +1,84 @@
+// 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<void> completer = Completer<void>();
+    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);
+  });
+}
+
+class _DeferringWidget extends StatefulWidget {
+  const _DeferringWidget({Key key, this.loader}) : super(key: key);
+
+  final Future<void> 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);
+  }
+}