diff --git a/packages/flutter/lib/src/rendering/layer.dart b/packages/flutter/lib/src/rendering/layer.dart index ddf5e3a39ad..eee4ee8288e 100644 --- a/packages/flutter/lib/src/rendering/layer.dart +++ b/packages/flutter/lib/src/rendering/layer.dart @@ -7,6 +7,7 @@ import 'dart:ui' as ui; import 'package:flutter/foundation.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/painting.dart'; +import 'package:flutter/scheduler.dart'; import 'package:vector_math/vector_math_64.dart'; import 'debug.dart'; @@ -2110,6 +2111,55 @@ class PhysicalModelLayer extends ContainerLayer { class LayerLink { LeaderLayer? _leader; + void _registerLeader(LeaderLayer leader) { + assert(_leader != leader); + assert((){ + if (_leader != null) { + _debugPreviousLeaders ??= {}; + _debugPreviousLeaders!.add(_leader!); + _debugScheduleLeadersCleanUpCheck(); + } + return true; + }()); + _leader = leader; + } + + void _unregisterLeader(LeaderLayer leader) { + assert(_leader != null); + if (_leader == leader) { + _leader = null; + } else { + assert((){ + _debugPreviousLeaders!.remove(leader); + return true; + }()); + } + } + + /// Stores the previous leaders that were replaced by the current [_leader] + /// in the current frame. + /// + /// These leaders need to give up their leaderships of this link by the end of + /// the current frame. + Set? _debugPreviousLeaders; + bool _debugLeaderCheckScheduled = false; + + /// Schedules the check as post frame callback to make sure the + /// [_debugPreviousLeaders] is empty. + void _debugScheduleLeadersCleanUpCheck() { + assert(_debugPreviousLeaders != null); + assert(() { + if (_debugLeaderCheckScheduled) + return true; + _debugLeaderCheckScheduled = true; + SchedulerBinding.instance!.addPostFrameCallback((Duration timeStamp) { + _debugLeaderCheckScheduled = false; + assert(_debugPreviousLeaders!.isEmpty); + }); + return true; + }()); + } + int _connectedFollowers = 0; /// Whether a [LeaderLayer] is currently connected to this link. @@ -2202,7 +2252,10 @@ class LeaderLayer extends ContainerLayer { if (_link == value) { return; } - _link._leader = null; + if (attached) { + _link._unregisterLeader(this); + value._registerLeader(this); + } _link = value; } @@ -2233,16 +2286,14 @@ class LeaderLayer extends ContainerLayer { @override void attach(Object owner) { super.attach(owner); - assert(link._leader == null); assert(_debugSetLastOffset(null)); - link._leader = this; + _link._registerLeader(this); } @override void detach() { - assert(link._leader == this); - link._leader = null; assert(_debugSetLastOffset(null)); + _link._unregisterLeader(this); super.detach(); } diff --git a/packages/flutter/test/rendering/layers_test.dart b/packages/flutter/test/rendering/layers_test.dart index 8f477a88bf4..cc63c23ed79 100644 --- a/packages/flutter/test/rendering/layers_test.dart +++ b/packages/flutter/test/rendering/layers_test.dart @@ -164,6 +164,18 @@ void main() { expect(followerLayer.debugSubtreeNeedsAddToScene, true); }); + test('switching layer link of an attached leader layer should not crash', () { + final LayerLink link = LayerLink(); + final LeaderLayer leaderLayer = LeaderLayer(link: link); + final RenderView view = RenderView(configuration: const ViewConfiguration(), window: window); + leaderLayer.attach(view); + final LayerLink link2 = LayerLink(); + leaderLayer.link = link2; + // This should not crash. + leaderLayer.detach(); + expect(leaderLayer.link, link2); + }); + test('leader layers are always dirty when connected to follower layer', () { final ContainerLayer root = ContainerLayer()..attach(Object()); diff --git a/packages/flutter/test/rendering/object_test.dart b/packages/flutter/test/rendering/object_test.dart index 59fe13cfd19..90313a21d0d 100644 --- a/packages/flutter/test/rendering/object_test.dart +++ b/packages/flutter/test/rendering/object_test.dart @@ -153,6 +153,76 @@ void main() { expect(renderObject.toStringShort(), contains('DISPOSED')); }); + test('Leader layer can switch to a different render object within one frame', () { + List? caughtErrors; + renderer.onErrors = () { + caughtErrors = renderer.takeAllFlutterErrorDetails().toList(); + }; + + final LayerLink layerLink = LayerLink(); + // renderObject1 paints the leader layer first. + final LeaderLayerRenderObject renderObject1 = LeaderLayerRenderObject(); + renderObject1.layerLink = layerLink; + renderObject1.attach(renderer.pipelineOwner); + final OffsetLayer rootLayer1 = OffsetLayer(); + rootLayer1.attach(renderObject1); + renderObject1.scheduleInitialPaint(rootLayer1); + renderObject1.layout(const BoxConstraints.tightForFinite()); + + final LeaderLayerRenderObject renderObject2 = LeaderLayerRenderObject(); + final OffsetLayer rootLayer2 = OffsetLayer(); + rootLayer2.attach(renderObject2); + renderObject2.attach(renderer.pipelineOwner); + renderObject2.scheduleInitialPaint(rootLayer2); + renderObject2.layout(const BoxConstraints.tightForFinite()); + renderer.pumpCompleteFrame(); + + // Swap the layer link to renderObject2 in the same frame + renderObject1.layerLink = null; + renderObject1.markNeedsPaint(); + renderObject2.layerLink = layerLink; + renderObject2.markNeedsPaint(); + renderer.pumpCompleteFrame(); + + // Swap the layer link to renderObject1 in the same frame + renderObject1.layerLink = layerLink; + renderObject1.markNeedsPaint(); + renderObject2.layerLink = null; + renderObject2.markNeedsPaint(); + renderer.pumpCompleteFrame(); + + renderer.onErrors = null; + expect(caughtErrors, isNull); + }); + + test('Leader layer append to two render objects does crash', () { + List? caughtErrors; + renderer.onErrors = () { + caughtErrors = renderer.takeAllFlutterErrorDetails().toList(); + }; + final LayerLink layerLink = LayerLink(); + // renderObject1 paints the leader layer first. + final LeaderLayerRenderObject renderObject1 = LeaderLayerRenderObject(); + renderObject1.layerLink = layerLink; + renderObject1.attach(renderer.pipelineOwner); + final OffsetLayer rootLayer1 = OffsetLayer(); + rootLayer1.attach(renderObject1); + renderObject1.scheduleInitialPaint(rootLayer1); + renderObject1.layout(const BoxConstraints.tightForFinite()); + + final LeaderLayerRenderObject renderObject2 = LeaderLayerRenderObject(); + renderObject2.layerLink = layerLink; + final OffsetLayer rootLayer2 = OffsetLayer(); + rootLayer2.attach(renderObject2); + renderObject2.attach(renderer.pipelineOwner); + renderObject2.scheduleInitialPaint(rootLayer2); + renderObject2.layout(const BoxConstraints.tightForFinite()); + renderer.pumpCompleteFrame(); + + renderer.onErrors = null; + expect(caughtErrors!.isNotEmpty, isTrue); + }); + test('RenderObject.dispose null the layer on repaint boundaries', () { final TestRenderObject renderObject = TestRenderObject(allowPaintBounds: true); // Force a layer to get set. @@ -255,6 +325,39 @@ class TestRenderObject extends RenderObject { } } +class LeaderLayerRenderObject extends RenderObject { + LeaderLayerRenderObject(); + + LayerLink? layerLink; + + @override + bool isRepaintBoundary = true; + + @override + void debugAssertDoesMeetConstraints() { } + + @override + Rect get paintBounds { + return Rect.zero; + } + + @override + void paint(PaintingContext context, Offset offset) { + if (layerLink != null) { + context.pushLayer(LeaderLayer(link: layerLink!), super.paint, offset); + } + } + + @override + void performLayout() { } + + @override + void performResize() { } + + @override + Rect get semanticBounds => const Rect.fromLTWH(0.0, 0.0, 10.0, 20.0); +} + class TestThrowingRenderObject extends RenderObject { @override void performLayout() { diff --git a/packages/flutter/test/rendering/rendering_tester.dart b/packages/flutter/test/rendering/rendering_tester.dart index 65ba5ae80d7..347dd878be7 100644 --- a/packages/flutter/test/rendering/rendering_tester.dart +++ b/packages/flutter/test/rendering/rendering_tester.dart @@ -82,6 +82,35 @@ class TestRenderingFlutterBinding extends BindingBase with SchedulerBinding, Ser EnginePhase phase = EnginePhase.composite; + /// Pumps a frame and runs its entire life cycle. + /// + /// This method runs all of the [SchedulerPhase]s in a frame, this is useful + /// to test [SchedulerPhase.postFrameCallbacks]. + void pumpCompleteFrame() { + final FlutterExceptionHandler? oldErrorHandler = FlutterError.onError; + FlutterError.onError = (FlutterErrorDetails details) { + _errors.add(details); + }; + try { + renderer.handleBeginFrame(null); + renderer.handleDrawFrame(); + } finally { + FlutterError.onError = oldErrorHandler; + if (_errors.isNotEmpty) { + if (onErrors != null) { + onErrors!(); + if (_errors.isNotEmpty) { + _errors.forEach(FlutterError.dumpErrorToConsole); + fail('There are more errors than the test inspected using TestRenderingFlutterBinding.takeFlutterErrorDetails.'); + } + } else { + _errors.forEach(FlutterError.dumpErrorToConsole); + fail('Caught error while rendering frame. See preceding logs for details.'); + } + } + } + } + @override void drawFrame() { assert(phase != EnginePhase.build, 'rendering_tester does not support testing the build phase; use flutter_test instead');