diff --git a/packages/flutter/lib/src/widgets/binding.dart b/packages/flutter/lib/src/widgets/binding.dart index 17c3310c131..2939408a106 100644 --- a/packages/flutter/lib/src/widgets/binding.dart +++ b/packages/flutter/lib/src/widgets/binding.dart @@ -991,11 +991,12 @@ mixin WidgetsBinding on BindingBase, ServicesBinding, SchedulerBinding, GestureB }()); TimingsCallback? firstFrameCallback; + bool debugFrameWasSentToEngine = false; if (_needToReportFirstFrame) { assert(!_firstFrameCompleter.isCompleted); firstFrameCallback = (List timings) { - assert(sendFramesToEngine); + assert(debugFrameWasSentToEngine); if (!kReleaseMode) { // Change the current user tag back to the default tag. At this point, // the user tag should be set to "AppStartUp" (originally set in the @@ -1020,6 +1021,10 @@ mixin WidgetsBinding on BindingBase, ServicesBinding, SchedulerBinding, GestureB buildOwner!.buildScope(rootElement!); } super.drawFrame(); + assert(() { + debugFrameWasSentToEngine = sendFramesToEngine; + return true; + }()); buildOwner!.finalizeTree(); } finally { assert(() { diff --git a/packages/flutter/test/widgets/binding_live_test.dart b/packages/flutter/test/widgets/binding_live_test.dart new file mode 100644 index 00000000000..366184b04cc --- /dev/null +++ b/packages/flutter/test/widgets/binding_live_test.dart @@ -0,0 +1,64 @@ +// Copyright 2014 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 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; + +// This file is for tests for WidgetsBinding that require a `LiveTestWidgetsFlutterBinding`. +void main() { + LiveTestWidgetsFlutterBinding(); + testWidgets('ReportTiming callback records the sendFramesToEngine when it was scheduled', (WidgetTester tester) async { + // Addresses https://github.com/flutter/flutter/issues/144261 + // This test needs LiveTestWidgetsFlutterBinding for multiple reasons. + // + // First, this was the environment that this bug was discovered. + // + // Second, unlike `AutomatedTestWidgetsFlutterBinding`, which overrides + // `scheduleWarmUpFrame` to execute the handlers synchronously, + // `LiveTestWidgetsFlutterBinding` still calls them asynchronously. This + // allows `runApp`, which also schedules a warm-up frame, to bind a widget + // without rendering a frame, which is needed to for `deferFirstFrame` to + // take effect. + + // Before `testWidgets` executes the test body, it pumps a frame with a + // fixed dummy widget, then calls `resetFirstFrameSent`. The pumped frame + // schedules a reportTiming call that has yet to arrive. + // + // This puts the test in an inconsistent state: a reportTiming callback is + // supposed to happen only after a frame is rendered, but due to + // `resetFirstFrameSent`, the framework thinks no frames have been rendered. + + expect(tester.binding.sendFramesToEngine, true); + // Push the widget with `runApp` instead of `tester.pump`, avoiding + // rendering a frame, which is needed for `deferFirstFrame` later to work. + runApp(const DummyWidget()); + // Verify that no widget tree is built and nothing is rendered. + expect(find.text('First frame'), findsNothing); + // Defer the first frame, making `sendFramesToEngine` false, so that widget + // tree will be built but not sent to the engine. + tester.binding.deferFirstFrame(); + expect(tester.binding.sendFramesToEngine, false); + // Pump a frame, letting the reportTiming callback to run. If the + // reportTiming callback were to assume that `sendFramesToEngine` is true, + // the callback would crash. + await tester.pump(const Duration(milliseconds: 1)); + await tester.binding.waitUntilFirstFrameRasterized; + expect(find.text('First frame'), findsOne); + }, skip: kIsWeb); // [intended] Web doesn't use LiveTestWidgetsFlutterBinding +} + +class DummyWidget extends StatelessWidget { + const DummyWidget({super.key}); + + @override + Widget build(BuildContext context) { + return const Directionality( + textDirection: TextDirection.ltr, + child: Center( + child: Text('First frame'), + ), + ); + } +}