Remove duplicate code in Element.rebuild() and BuildOwner.buildScope() (#71940)
Element.rebuild() and BuildOwner.buildScope() both had code to
manager the `BuildOwner._debugCurrentBuildTarget` state variable.
Unfortunately, only one of the two places was using a try/finally
to ensure that the state variable was restored even if an error
was thrown.
This creates a private method to unify the copy/pasted code.
diff --git a/packages/flutter/lib/src/gestures/pointer_router.dart b/packages/flutter/lib/src/gestures/pointer_router.dart
index daf09bf..a59d07d 100644
--- a/packages/flutter/lib/src/gestures/pointer_router.dart
+++ b/packages/flutter/lib/src/gestures/pointer_router.dart
@@ -75,6 +75,7 @@
///
/// This is valid in debug builds only. In release builds, this will throw an
/// [UnsupportedError].
+ @visibleForTesting
int get debugGlobalRouteCount {
int? count;
assert(() {
diff --git a/packages/flutter/lib/src/widgets/framework.dart b/packages/flutter/lib/src/widgets/framework.dart
index d2d4ed2..73855e2 100644
--- a/packages/flutter/lib/src/widgets/framework.dart
+++ b/packages/flutter/lib/src/widgets/framework.dart
@@ -2651,6 +2651,26 @@
/// Only valid when asserts are enabled.
bool get debugBuilding => _debugBuilding;
bool _debugBuilding = false;
+
+ /// The element currently being built, or null if no element is actively
+ /// being built.
+ ///
+ /// This is valid in debug builds only. In release builds, this will throw an
+ /// [UnsupportedError].
+ @visibleForTesting
+ Element? get debugCurrentBuildTarget {
+ Element? result;
+ bool isSupportedOperation = false;
+ assert(() {
+ result = _debugCurrentBuildTarget;
+ isSupportedOperation = true;
+ return true;
+ }());
+ if (isSupportedOperation) {
+ return result;
+ }
+ throw UnsupportedError('debugCurrentBuildTarget is not supported in release builds');
+ }
Element? _debugCurrentBuildTarget;
/// Establishes a scope in which calls to [State.setState] are forbidden, and
@@ -2676,6 +2696,25 @@
assert(_debugStateLockLevel >= 0);
}
+ void _runWithCurrentBuildTarget(Element element, VoidCallback callback) {
+ assert(_debugStateLocked);
+ Element? debugPreviousBuildTarget;
+ assert(() {
+ debugPreviousBuildTarget = _debugCurrentBuildTarget;
+ _debugCurrentBuildTarget = element;
+ return true;
+ }());
+ try {
+ callback();
+ } finally {
+ assert(() {
+ assert(_debugCurrentBuildTarget == element);
+ _debugCurrentBuildTarget = debugPreviousBuildTarget;
+ return true;
+ }());
+ }
+ }
+
/// Establishes a scope for updating the widget tree, and calls the given
/// `callback`, if any. Then, builds all the elements that were marked as
/// dirty using [scheduleBuildFor], in depth order.
@@ -2718,26 +2757,22 @@
try {
_scheduledFlushDirtyElements = true;
if (callback != null) {
- assert(_debugStateLocked);
- Element? debugPreviousBuildTarget;
- assert(() {
- context._debugSetAllowIgnoredCallsToMarkNeedsBuild(true);
- debugPreviousBuildTarget = _debugCurrentBuildTarget;
- _debugCurrentBuildTarget = context;
- return true;
- }());
- _dirtyElementsNeedsResorting = false;
- try {
- callback();
- } finally {
+ _runWithCurrentBuildTarget(context, () {
assert(() {
- context._debugSetAllowIgnoredCallsToMarkNeedsBuild(false);
- assert(_debugCurrentBuildTarget == context);
- _debugCurrentBuildTarget = debugPreviousBuildTarget;
- _debugElementWasRebuilt(context);
+ context._debugSetAllowIgnoredCallsToMarkNeedsBuild(true);
return true;
}());
- }
+ _dirtyElementsNeedsResorting = false;
+ try {
+ callback();
+ } finally {
+ assert(() {
+ context._debugSetAllowIgnoredCallsToMarkNeedsBuild(false);
+ _debugElementWasRebuilt(context);
+ return true;
+ }());
+ }
+ });
}
_dirtyElements.sort(Element._sort);
_dirtyElementsNeedsResorting = false;
@@ -4369,19 +4404,7 @@
return true;
}());
assert(_lifecycleState == _ElementLifecycle.active);
- assert(owner!._debugStateLocked);
- Element? debugPreviousBuildTarget;
- assert(() {
- debugPreviousBuildTarget = owner!._debugCurrentBuildTarget;
- owner!._debugCurrentBuildTarget = this;
- return true;
- }());
- performRebuild();
- assert(() {
- assert(owner!._debugCurrentBuildTarget == this);
- owner!._debugCurrentBuildTarget = debugPreviousBuildTarget;
- return true;
- }());
+ owner!._runWithCurrentBuildTarget(this, performRebuild);
assert(!_dirty);
}
@@ -5586,6 +5609,7 @@
}
@override
+ @mustCallSuper
void performRebuild() {
assert(() {
_debugDoingBuild = true;
diff --git a/packages/flutter/test/widgets/framework_test.dart b/packages/flutter/test/widgets/framework_test.dart
index 7dc9e7e..b213f94 100644
--- a/packages/flutter/test/widgets/framework_test.dart
+++ b/packages/flutter/test/widgets/framework_test.dart
@@ -1494,6 +1494,44 @@
expect(GestureBinding.instance!.pointerRouter.debugGlobalRouteCount, pointerRouterCount);
expect(RawKeyboard.instance.keyEventHandler, same(rawKeyEventHandler));
});
+
+ testWidgets('Errors in build', (WidgetTester tester) async {
+ final ErrorWidgetBuilder oldErrorBuilder = ErrorWidget.builder;
+ ErrorWidget.builder = (FlutterErrorDetails detail) => throw AssertionError();
+ try {
+ final FlutterExceptionHandler? oldErrorHandler = FlutterError.onError;
+ FlutterError.onError = (FlutterErrorDetails detail) {};
+ try {
+ int buildCount = 0;
+ void handleBuild() => buildCount++;
+ expect(tester.binding.buildOwner!.debugCurrentBuildTarget, isNull);
+ await tester.pumpWidget(_WidgetThatCanThrowInBuild(onBuild: handleBuild));
+ expect(buildCount, 1);
+ await tester.pumpWidget(_WidgetThatCanThrowInBuild(onBuild: handleBuild, shouldThrow: true));
+ tester.takeException();
+ expect(buildCount, 2);
+ expect(tester.binding.buildOwner!.debugCurrentBuildTarget, isNull);
+ } finally {
+ FlutterError.onError = oldErrorHandler;
+ }
+ } finally {
+ ErrorWidget.builder = oldErrorBuilder;
+ }
+ });
+}
+
+class _WidgetThatCanThrowInBuild extends StatelessWidget {
+ const _WidgetThatCanThrowInBuild({required this.onBuild, this.shouldThrow = false});
+
+ final VoidCallback onBuild;
+ final bool shouldThrow;
+
+ @override
+ Widget build(BuildContext context) {
+ onBuild();
+ assert(!shouldThrow);
+ return Container();
+ }
}
class _FakeFocusManager implements FocusManager {