Maintain dirtiness and use retained engine layers (#23434)

For #21756
diff --git a/packages/flutter/lib/src/rendering/layer.dart b/packages/flutter/lib/src/rendering/layer.dart
index c018819..f4533ea 100644
--- a/packages/flutter/lib/src/rendering/layer.dart
+++ b/packages/flutter/lib/src/rendering/layer.dart
@@ -4,7 +4,7 @@
 
 import 'dart:async';
 import 'dart:collection';
-import 'dart:ui' as ui show Image, ImageFilter, Picture, Scene, SceneBuilder;
+import 'dart:ui' as ui show EngineLayer, Image, ImageFilter, Picture, Scene, SceneBuilder;
 
 import 'package:flutter/foundation.dart';
 import 'package:flutter/painting.dart';
@@ -41,6 +41,59 @@
   @override
   ContainerLayer get parent => super.parent;
 
+  // Whether this layer has any changes since its last call to [addToScene].
+  //
+  // Initialized to true as a new layer has never called [addToScene].
+  bool _needsAddToScene = true;
+
+  /// Mark that this layer has changed and [addToScene] needs to be called.
+  @protected
+  void markNeedsAddToScene() {
+    _needsAddToScene = true;
+  }
+
+  /// Mark that this layer is in sync with engine.
+  ///
+  /// This is only for debug and test purpose only.
+  @visibleForTesting
+  void debugMarkClean() {
+    assert((){
+      _needsAddToScene = false;
+      return true;
+    }());
+  }
+
+  /// Subclasses may override this to true to disable retained rendering.
+  @protected
+  bool get alwaysNeedsAddToScene => false;
+
+  bool _subtreeNeedsAddToScene;
+
+  /// Whether any layer in the subtree needs [addToScene].
+  ///
+  /// This is for debug and test purpose only. It only becomes valid after
+  /// calling [updateSubtreeNeedsAddToScene].
+  @visibleForTesting
+  bool get debugSubtreeNeedsAddToScene {
+    bool result;
+    assert((){
+      result = _subtreeNeedsAddToScene;
+      return true;
+    }());
+    return result;
+  }
+
+  ui.EngineLayer _engineLayer;
+
+  /// Traverse the layer tree and compute if any subtree needs [addToScene].
+  ///
+  /// A subtree needs [addToScene] if any of its layer needs [addToScene].
+  /// The [ContainerLayer] will override this to respect its children.
+  @protected
+  void updateSubtreeNeedsAddToScene() {
+    _subtreeNeedsAddToScene = _needsAddToScene || alwaysNeedsAddToScene;
+  }
+
   /// This layer's next sibling in the parent layer's child list.
   Layer get nextSibling => _nextSibling;
   Layer _nextSibling;
@@ -49,6 +102,18 @@
   Layer get previousSibling => _previousSibling;
   Layer _previousSibling;
 
+  @override
+  void dropChild(AbstractNode child) {
+    markNeedsAddToScene();
+    super.dropChild(child);
+  }
+
+  @override
+  void adoptChild(AbstractNode child) {
+    markNeedsAddToScene();
+    super.adoptChild(child);
+  }
+
   /// Removes this layer from its parent layer's child list.
   ///
   /// This has no effect if the layer's parent is already null.
@@ -104,7 +169,29 @@
   S find<S>(Offset regionOffset);
 
   /// Override this method to upload this layer to the engine.
-  void addToScene(ui.SceneBuilder builder, [Offset layerOffset = Offset.zero]);
+  ///
+  /// Return the engine layer for retained rendering. When there's no
+  /// corresponding engine layer, null is returned.
+  @protected
+  ui.EngineLayer addToScene(ui.SceneBuilder builder, [Offset layerOffset = Offset.zero]);
+
+  void _addToSceneWithRetainedRendering(ui.SceneBuilder builder) {
+    // There can't be a loop by adding a retained layer subtree whose
+    // _subtreeNeedsAddToScene is false.
+    //
+    // Proof by contradiction:
+    //
+    // If we introduce a loop, this retained layer must be appended to one of
+    // its descendent layers, say A. That means the child structure of A has
+    // changed so A's _needsAddToScene is true. This contradicts
+    // _subtreeNeedsAddToScene being false.
+    if (!_subtreeNeedsAddToScene && _engineLayer != null) {
+      builder.addRetained(_engineLayer);
+      return;
+    }
+    _engineLayer = addToScene(builder);
+    _needsAddToScene = false;
+  }
 
   /// The object responsible for creating this layer.
   ///
@@ -144,7 +231,12 @@
   ///
   /// The scene must be explicitly recomposited after this property is changed
   /// (as described at [Layer]).
-  ui.Picture picture;
+  ui.Picture get picture => _picture;
+  ui.Picture _picture;
+  set picture(ui.Picture picture) {
+    _needsAddToScene = true;
+    _picture = picture;
+  }
 
   /// Hints that the painting in this layer is complex and would benefit from
   /// caching.
@@ -154,7 +246,14 @@
   ///
   /// The scene must be explicitly recomposited after this property is changed
   /// (as described at [Layer]).
-  bool isComplexHint = false;
+  bool get isComplexHint => _isComplexHint;
+  bool _isComplexHint = false;
+  set isComplexHint(bool value) {
+    if (value != _isComplexHint) {
+      _isComplexHint = value;
+      markNeedsAddToScene();
+    }
+  }
 
   /// Hints that the painting in this layer is likely to change next frame.
   ///
@@ -165,11 +264,19 @@
   ///
   /// The scene must be explicitly recomposited after this property is changed
   /// (as described at [Layer]).
-  bool willChangeHint = false;
+  bool get willChangeHint => _willChangeHint;
+  bool _willChangeHint = false;
+  set willChangeHint(bool value) {
+    if (value != _willChangeHint) {
+      _willChangeHint = value;
+      markNeedsAddToScene();
+    }
+  }
 
   @override
-  void addToScene(ui.SceneBuilder builder, [Offset layerOffset = Offset.zero]) {
+  ui.EngineLayer addToScene(ui.SceneBuilder builder, [Offset layerOffset = Offset.zero]) {
     builder.addPicture(layerOffset, picture, isComplexHint: isComplexHint, willChangeHint: willChangeHint);
+    return null; // this does not return an engine layer yet.
   }
 
   @override
@@ -234,7 +341,7 @@
   final bool freeze;
 
   @override
-  void addToScene(ui.SceneBuilder builder, [Offset layerOffset = Offset.zero]) {
+  ui.EngineLayer addToScene(ui.SceneBuilder builder, [Offset layerOffset = Offset.zero]) {
     final Rect shiftedRect = rect.shift(layerOffset);
     builder.addTexture(
       textureId,
@@ -243,6 +350,7 @@
       height: shiftedRect.height,
       freeze: freeze,
     );
+    return null; // this does not return an engine layer yet.
   }
 
   @override
@@ -256,18 +364,25 @@
 class PerformanceOverlayLayer extends Layer {
   /// Creates a layer that displays a performance overlay.
   PerformanceOverlayLayer({
-    @required this.overlayRect,
+    @required Rect overlayRect,
     @required this.optionsMask,
     @required this.rasterizerThreshold,
     @required this.checkerboardRasterCacheImages,
     @required this.checkerboardOffscreenLayers,
-  });
+  }) : _overlayRect = overlayRect;
 
   /// The rectangle in this layer's coordinate system that the overlay should occupy.
   ///
   /// The scene must be explicitly recomposited after this property is changed
   /// (as described at [Layer]).
-  Rect overlayRect;
+  Rect get overlayRect => _overlayRect;
+  Rect _overlayRect;
+  set overlayRect(Rect value) {
+    if (value != _overlayRect) {
+      _overlayRect = value;
+      markNeedsAddToScene();
+    }
+  }
 
   /// The mask is created by shifting 1 by the index of the specific
   /// [PerformanceOverlayOption] to enable.
@@ -302,12 +417,13 @@
   final bool checkerboardOffscreenLayers;
 
   @override
-  void addToScene(ui.SceneBuilder builder, [Offset layerOffset = Offset.zero]) {
+  ui.EngineLayer addToScene(ui.SceneBuilder builder, [Offset layerOffset = Offset.zero]) {
     assert(optionsMask != null);
     builder.addPerformanceOverlay(optionsMask, overlayRect.shift(layerOffset));
     builder.setRasterizerTracingThreshold(rasterizerThreshold);
     builder.setCheckerboardRasterCacheImages(checkerboardRasterCacheImages);
     builder.setCheckerboardOffscreenLayers(checkerboardOffscreenLayers);
+    return null; // this does not return an engine layer yet.
   }
 
   @override
@@ -349,6 +465,17 @@
   }
 
   @override
+  void updateSubtreeNeedsAddToScene() {
+    super.updateSubtreeNeedsAddToScene();
+    Layer child = firstChild;
+    while (child != null) {
+      child.updateSubtreeNeedsAddToScene();
+      _subtreeNeedsAddToScene = _subtreeNeedsAddToScene || child._subtreeNeedsAddToScene;
+      child = child.nextSibling;
+    }
+  }
+
+  @override
   S find<S>(Offset regionOffset) {
     Layer current = lastChild;
     while (current != null) {
@@ -451,8 +578,9 @@
   }
 
   @override
-  void addToScene(ui.SceneBuilder builder, [Offset layerOffset = Offset.zero]) {
+  ui.EngineLayer addToScene(ui.SceneBuilder builder, [Offset layerOffset = Offset.zero]) {
     addChildrenToScene(builder, layerOffset);
+    return null; // ContainerLayer does not have a corresponding engine layer
   }
 
   /// Uploads all of this layer's children to the engine.
@@ -465,7 +593,11 @@
   void addChildrenToScene(ui.SceneBuilder builder, [Offset childOffset = Offset.zero]) {
     Layer child = firstChild;
     while (child != null) {
-      child.addToScene(builder, childOffset);
+      if (childOffset == Offset.zero) {
+        child._addToSceneWithRetainedRendering(builder);
+      } else {
+        child.addToScene(builder, childOffset);
+      }
       child = child.nextSibling;
     }
   }
@@ -540,7 +672,7 @@
   ///
   /// By default, [offset] is zero. It must be non-null before the compositing
   /// phase of the pipeline.
-  OffsetLayer({ this.offset = Offset.zero });
+  OffsetLayer({ Offset offset = Offset.zero }) : _offset = offset;
 
   /// Offset from parent in the parent's coordinate system.
   ///
@@ -549,7 +681,14 @@
   ///
   /// The [offset] property must be non-null before the compositing phase of the
   /// pipeline.
-  Offset offset;
+  Offset get offset => _offset;
+  Offset _offset;
+  set offset(Offset value) {
+    if (value != _offset) {
+      markNeedsAddToScene();
+    }
+    _offset = value;
+  }
 
   @override
   S find<S>(Offset regionOffset) {
@@ -563,16 +702,25 @@
     transform.multiply(Matrix4.translationValues(offset.dx, offset.dy, 0.0));
   }
 
+  /// Consider this layer as the root and build a scene (a tree of layers)
+  /// in the engine.
+  ui.Scene buildScene(ui.SceneBuilder builder) {
+    updateSubtreeNeedsAddToScene();
+    addToScene(builder);
+    return builder.build();
+  }
+
   @override
-  void addToScene(ui.SceneBuilder builder, [Offset layerOffset = Offset.zero]) {
+  ui.EngineLayer addToScene(ui.SceneBuilder builder, [Offset layerOffset = Offset.zero]) {
     // Skia has a fast path for concatenating scale/translation only matrices.
     // Hence pushing a translation-only transform layer should be fast. For
     // retained rendering, we don't want to push the offset down to each leaf
     // node. Otherwise, changing an offset layer on the very high level could
     // cascade the change to too many leaves.
-    builder.pushOffset(layerOffset.dx + offset.dx, layerOffset.dy + offset.dy);
+    final ui.EngineLayer engineLayer = builder.pushOffset(layerOffset.dx + offset.dx, layerOffset.dy + offset.dy);
     addChildrenToScene(builder);
     builder.pop();
+    return engineLayer;
   }
 
   @override
@@ -608,8 +756,7 @@
     );
     transform.scale(pixelRatio, pixelRatio);
     builder.pushTransform(transform.storage);
-    addToScene(builder);
-    final ui.Scene scene = builder.build();
+    final ui.Scene scene = buildScene(builder);
     try {
       // Size is rounded up to the next pixel to make sure we don't clip off
       // anything.
@@ -633,14 +780,22 @@
   ///
   /// The [clipRect] property must be non-null before the compositing phase of
   /// the pipeline.
-  ClipRectLayer({ this.clipRect, Clip clipBehavior = Clip.hardEdge }) :
-        _clipBehavior = clipBehavior, assert(clipBehavior != null), assert(clipBehavior != Clip.none);
+  ClipRectLayer({ @required Rect clipRect, Clip clipBehavior = Clip.hardEdge }) :
+        _clipRect = clipRect, _clipBehavior = clipBehavior,
+        assert(clipBehavior != null), assert(clipBehavior != Clip.none);
 
   /// The rectangle to clip in the parent's coordinate system.
   ///
   /// The scene must be explicitly recomposited after this property is changed
   /// (as described at [Layer]).
-  Rect clipRect;
+  Rect get clipRect => _clipRect;
+  Rect _clipRect;
+  set clipRect(Rect value) {
+    if (value != _clipRect) {
+      _clipRect = value;
+      markNeedsAddToScene();
+    }
+  }
 
   /// {@template flutter.clipper.clipBehavior}
   /// Controls how to clip (default to [Clip.antiAlias]).
@@ -652,7 +807,10 @@
   set clipBehavior(Clip value) {
     assert(value != null);
     assert(value != Clip.none);
-    _clipBehavior = value;
+    if (value != _clipBehavior) {
+      _clipBehavior = value;
+      markNeedsAddToScene();
+    }
   }
 
   @override
@@ -663,7 +821,7 @@
   }
 
   @override
-  void addToScene(ui.SceneBuilder builder, [Offset layerOffset = Offset.zero]) {
+  ui.EngineLayer addToScene(ui.SceneBuilder builder, [Offset layerOffset = Offset.zero]) {
     bool enabled = true;
     assert(() {
       enabled = !debugDisableClipLayers;
@@ -674,6 +832,7 @@
     addChildrenToScene(builder, layerOffset);
     if (enabled)
       builder.pop();
+    return null; // this does not return an engine layer yet.
   }
 
   @override
@@ -693,14 +852,22 @@
   ///
   /// The [clipRRect] property must be non-null before the compositing phase of
   /// the pipeline.
-  ClipRRectLayer({ this.clipRRect, Clip clipBehavior = Clip.antiAlias }) :
-        _clipBehavior = clipBehavior, assert(clipBehavior != null), assert(clipBehavior != Clip.none);
+  ClipRRectLayer({ @required RRect clipRRect, Clip clipBehavior = Clip.antiAlias }) :
+        _clipRRect = clipRRect, _clipBehavior = clipBehavior,
+        assert(clipBehavior != null), assert(clipBehavior != Clip.none);
 
   /// The rounded-rect to clip in the parent's coordinate system.
   ///
   /// The scene must be explicitly recomposited after this property is changed
   /// (as described at [Layer]).
-  RRect clipRRect;
+  RRect get clipRRect => _clipRRect;
+  RRect _clipRRect;
+  set clipRRect(RRect value) {
+    if (value != _clipRRect) {
+      _clipRRect = value;
+      markNeedsAddToScene();
+    }
+  }
 
   /// {@macro flutter.clipper.clipBehavior}
   Clip get clipBehavior => _clipBehavior;
@@ -708,7 +875,10 @@
   set clipBehavior(Clip value) {
     assert(value != null);
     assert(value != Clip.none);
-    _clipBehavior = value;
+    if (value != _clipBehavior) {
+      _clipBehavior = value;
+      markNeedsAddToScene();
+    }
   }
 
   @override
@@ -719,7 +889,7 @@
   }
 
   @override
-  void addToScene(ui.SceneBuilder builder, [Offset layerOffset = Offset.zero]) {
+  ui.EngineLayer addToScene(ui.SceneBuilder builder, [Offset layerOffset = Offset.zero]) {
     bool enabled = true;
     assert(() {
       enabled = !debugDisableClipLayers;
@@ -730,6 +900,7 @@
     addChildrenToScene(builder, layerOffset);
     if (enabled)
       builder.pop();
+    return null; // this does not return an engine layer yet.
   }
 
   @override
@@ -749,14 +920,22 @@
   ///
   /// The [clipPath] property must be non-null before the compositing phase of
   /// the pipeline.
-  ClipPathLayer({ this.clipPath, Clip clipBehavior = Clip.antiAlias }) :
-        _clipBehavior = clipBehavior, assert(clipBehavior != null), assert(clipBehavior != Clip.none);
+  ClipPathLayer({ @required Path clipPath, Clip clipBehavior = Clip.antiAlias }) :
+        _clipPath = clipPath, _clipBehavior = clipBehavior,
+        assert(clipBehavior != null), assert(clipBehavior != Clip.none);
 
   /// The path to clip in the parent's coordinate system.
   ///
   /// The scene must be explicitly recomposited after this property is changed
   /// (as described at [Layer]).
-  Path clipPath;
+  Path get clipPath => _clipPath;
+  Path _clipPath;
+  set clipPath(Path value) {
+    if (value != _clipPath) {
+      _clipPath = value;
+      markNeedsAddToScene();
+    }
+  }
 
   /// {@macro flutter.clipper.clipBehavior}
   Clip get clipBehavior => _clipBehavior;
@@ -764,7 +943,10 @@
   set clipBehavior(Clip value) {
     assert(value != null);
     assert(value != Clip.none);
-    _clipBehavior = value;
+    if (value != _clipBehavior) {
+      _clipBehavior = value;
+      markNeedsAddToScene();
+    }
   }
 
   @override
@@ -775,7 +957,7 @@
   }
 
   @override
-  void addToScene(ui.SceneBuilder builder, [Offset layerOffset = Offset.zero]) {
+  ui.EngineLayer addToScene(ui.SceneBuilder builder, [Offset layerOffset = Offset.zero]) {
     bool enabled = true;
     assert(() {
       enabled = !debugDisableClipLayers;
@@ -786,6 +968,7 @@
     addChildrenToScene(builder, layerOffset);
     if (enabled)
       builder.pop();
+    return null; // this does not return an engine layer yet.
   }
 }
 
@@ -826,7 +1009,7 @@
   bool _inverseDirty = true;
 
   @override
-  void addToScene(ui.SceneBuilder builder, [Offset layerOffset = Offset.zero]) {
+  ui.EngineLayer addToScene(ui.SceneBuilder builder, [Offset layerOffset = Offset.zero]) {
     _lastEffectiveTransform = transform;
     final Offset totalOffset = offset + layerOffset;
     if (totalOffset != Offset.zero) {
@@ -836,6 +1019,7 @@
     builder.pushTransform(_lastEffectiveTransform.storage);
     addChildrenToScene(builder);
     builder.pop();
+    return null; // this does not return an engine layer yet.
   }
 
   @override
@@ -875,7 +1059,8 @@
   ///
   /// The [alpha] property must be non-null before the compositing phase of
   /// the pipeline.
-  OpacityLayer({ this.alpha });
+  OpacityLayer({ @required int alpha, Offset offset = Offset.zero })
+      : _alpha = alpha, _offset = offset;
 
   /// The amount to multiply into the alpha channel.
   ///
@@ -884,20 +1069,38 @@
   ///
   /// The scene must be explicitly recomposited after this property is changed
   /// (as described at [Layer]).
-  int alpha;
+  int get alpha => _alpha;
+  int _alpha;
+  set alpha(int value) {
+    if (value != _alpha) {
+      _alpha = value;
+      markNeedsAddToScene();
+    }
+  }
+
+  /// Offset from parent in the parent's coordinate system.
+  Offset get offset => _offset;
+  Offset _offset;
+  set offset(Offset value) {
+    if (value != _offset) {
+      _offset = value;
+      markNeedsAddToScene();
+    }
+  }
 
   @override
-  void addToScene(ui.SceneBuilder builder, [Offset layerOffset = Offset.zero]) {
+  ui.EngineLayer addToScene(ui.SceneBuilder builder, [Offset layerOffset = Offset.zero]) {
     bool enabled = true;
     assert(() {
       enabled = !debugDisableOpacityLayers;
       return true;
     }());
     if (enabled)
-      builder.pushOpacity(alpha);
-    addChildrenToScene(builder, layerOffset);
+      builder.pushOpacity(alpha, offset: offset + layerOffset);
+    addChildrenToScene(builder);
     if (enabled)
       builder.pop();
+    return null; // this does not return an engine layer yet.
   }
 
   @override
@@ -913,31 +1116,54 @@
   ///
   /// The [shader], [maskRect], and [blendMode] properties must be non-null
   /// before the compositing phase of the pipeline.
-  ShaderMaskLayer({ this.shader, this.maskRect, this.blendMode });
+  ShaderMaskLayer({ @required Shader shader, @required Rect maskRect, @required BlendMode blendMode })
+      : _shader = shader, _maskRect = maskRect, _blendMode = blendMode;
 
   /// The shader to apply to the children.
   ///
   /// The scene must be explicitly recomposited after this property is changed
   /// (as described at [Layer]).
-  Shader shader;
+  Shader get shader => _shader;
+  Shader _shader;
+  set shader(Shader value) {
+    if (value != _shader) {
+      _shader = value;
+      markNeedsAddToScene();
+    }
+  }
 
   /// The size of the shader.
   ///
   /// The scene must be explicitly recomposited after this property is changed
   /// (as described at [Layer]).
-  Rect maskRect;
+  Rect get maskRect => _maskRect;
+  Rect _maskRect;
+  set maskRect(Rect value) {
+    if (value != _maskRect) {
+      _maskRect = value;
+      markNeedsAddToScene();
+    }
+  }
 
   /// The blend mode to apply when blending the shader with the children.
   ///
   /// The scene must be explicitly recomposited after this property is changed
   /// (as described at [Layer]).
-  BlendMode blendMode;
+  BlendMode get blendMode => _blendMode;
+  BlendMode _blendMode;
+  set blendMode(BlendMode value) {
+    if (value != _blendMode) {
+      _blendMode = value;
+      markNeedsAddToScene();
+    }
+  }
 
   @override
-  void addToScene(ui.SceneBuilder builder, [Offset layerOffset = Offset.zero]) {
+  ui.EngineLayer addToScene(ui.SceneBuilder builder, [Offset layerOffset = Offset.zero]) {
     builder.pushShaderMask(shader, maskRect.shift(layerOffset), blendMode);
     addChildrenToScene(builder, layerOffset);
     builder.pop();
+    return null; // this does not return an engine layer yet.
   }
 
   @override
@@ -955,19 +1181,27 @@
   ///
   /// The [filter] property must be non-null before the compositing phase of the
   /// pipeline.
-  BackdropFilterLayer({ this.filter });
+  BackdropFilterLayer({ @required ui.ImageFilter filter }) : _filter = filter;
 
   /// The filter to apply to the existing contents of the scene.
   ///
   /// The scene must be explicitly recomposited after this property is changed
   /// (as described at [Layer]).
-  ui.ImageFilter filter;
+  ui.ImageFilter get filter => _filter;
+  ui.ImageFilter _filter;
+  set filter(ui.ImageFilter value) {
+    if (value != _filter) {
+      _filter = value;
+      markNeedsAddToScene();
+    }
+  }
 
   @override
-  void addToScene(ui.SceneBuilder builder, [Offset layerOffset = Offset.zero]) {
+  ui.EngineLayer addToScene(ui.SceneBuilder builder, [Offset layerOffset = Offset.zero]) {
     builder.pushBackdropFilter(filter);
     addChildrenToScene(builder, layerOffset);
     builder.pop();
+    return null; // this does not return an engine layer yet.
   }
 }
 
@@ -986,25 +1220,44 @@
   ///
   /// The [clipPath], [elevation], and [color] arguments must not be null.
   PhysicalModelLayer({
-    @required this.clipPath,
-    this.clipBehavior = Clip.none,
-    @required this.elevation,
-    @required this.color,
-    @required this.shadowColor,
+    @required Path clipPath,
+    Clip clipBehavior = Clip.none,
+    @required double elevation,
+    @required Color color,
+    @required Color shadowColor,
   }) : assert(clipPath != null),
        assert(clipBehavior != null),
        assert(elevation != null),
        assert(color != null),
-       assert(shadowColor != null);
+       assert(shadowColor != null),
+       _clipPath = clipPath,
+       _clipBehavior = clipBehavior,
+       _elevation = elevation,
+       _color = color,
+       _shadowColor = shadowColor;
 
   /// The path to clip in the parent's coordinate system.
   ///
   /// The scene must be explicitly recomposited after this property is changed
   /// (as described at [Layer]).
-  Path clipPath;
+  Path get clipPath => _clipPath;
+  Path _clipPath;
+  set clipPath(Path value) {
+    if (value != _clipPath) {
+      _clipPath = value;
+      markNeedsAddToScene();
+    }
+  }
 
   /// {@macro flutter.widgets.Clip}
-  Clip clipBehavior;
+  Clip get clipBehavior => _clipBehavior;
+  Clip _clipBehavior;
+  set clipBehavior(Clip value) {
+    if (value != _clipBehavior) {
+      _clipBehavior = value;
+      markNeedsAddToScene();
+    }
+  }
 
   /// The z-coordinate at which to place this physical object.
   ///
@@ -1016,16 +1269,37 @@
   /// flag is set. For this reason, this property will often be set to zero in
   /// tests even if the layer should be raised. To verify the actual value,
   /// consider setting [debugDisableShadows] to false in your test.
-  double elevation;
+  double get elevation => _elevation;
+  double _elevation;
+  set elevation(double value) {
+    if (value != _elevation) {
+      _elevation = value;
+      markNeedsAddToScene();
+    }
+  }
 
   /// The background color.
   ///
   /// The scene must be explicitly recomposited after this property is changed
   /// (as described at [Layer]).
-  Color color;
+  Color get color => _color;
+  Color _color;
+  set color(Color value) {
+    if (value != _color) {
+      _color = value;
+      markNeedsAddToScene();
+    }
+  }
 
   /// The shadow color.
-  Color shadowColor;
+  Color get shadowColor => _shadowColor;
+  Color _shadowColor;
+  set shadowColor(Color value) {
+    if (value != _shadowColor) {
+      _shadowColor = value;
+      markNeedsAddToScene();
+    }
+  }
 
   @override
   S find<S>(Offset regionOffset) {
@@ -1035,14 +1309,15 @@
   }
 
   @override
-  void addToScene(ui.SceneBuilder builder, [Offset layerOffset = Offset.zero]) {
+  ui.EngineLayer addToScene(ui.SceneBuilder builder, [Offset layerOffset = Offset.zero]) {
+    ui.EngineLayer engineLayer;
     bool enabled = true;
     assert(() {
       enabled = !debugDisablePhysicalShapeLayers;
       return true;
     }());
     if (enabled) {
-      builder.pushPhysicalShape(
+      engineLayer = builder.pushPhysicalShape(
         path: clipPath.shift(layerOffset),
         elevation: elevation,
         color: color,
@@ -1053,6 +1328,7 @@
     addChildrenToScene(builder, layerOffset);
     if (enabled)
       builder.pop();
+    return engineLayer;
   }
 
   @override
@@ -1115,6 +1391,10 @@
   /// pipeline.
   Offset offset;
 
+  /// {@macro flutter.leaderFollower.alwaysNeedsAddToScene}
+  @override
+  bool get alwaysNeedsAddToScene => true;
+
   @override
   void attach(Object owner) {
     super.attach(owner);
@@ -1144,7 +1424,7 @@
   }
 
   @override
-  void addToScene(ui.SceneBuilder builder, [Offset layerOffset = Offset.zero]) {
+  ui.EngineLayer addToScene(ui.SceneBuilder builder, [Offset layerOffset = Offset.zero]) {
     assert(offset != null);
     _lastOffset = offset + layerOffset;
     if (_lastOffset != Offset.zero)
@@ -1152,6 +1432,7 @@
     addChildrenToScene(builder);
     if (_lastOffset != Offset.zero)
       builder.pop();
+    return null; // this does not have an engine layer.
   }
 
   /// Applies the transform that would be applied when compositing the given
@@ -1347,15 +1628,28 @@
     _inverseDirty = true;
   }
 
+  /// {@template flutter.leaderFollower.alwaysNeedsAddToScene}
+  /// This disables retained rendering for Leader/FollowerLayer.
+  ///
+  /// A FollowerLayer copies changes from a LeaderLayer that could be anywhere
+  /// in the Layer tree, and that LeaderLayer could change without notifying the
+  /// FollowerLayer. Therefore we have to always call a FollowerLayer's
+  /// [addToScene]. In order to call FollowerLayer's [addToScene], LeaderLayer's
+  /// [addToScene] must be called first so LeaderLayer must also be considered
+  /// as [alwaysNeedsAddToScene].
+  /// {@endtemplate}
   @override
-  void addToScene(ui.SceneBuilder builder, [Offset layerOffset = Offset.zero]) {
+  bool get alwaysNeedsAddToScene => true;
+
+  @override
+  ui.EngineLayer addToScene(ui.SceneBuilder builder, [Offset layerOffset = Offset.zero]) {
     assert(link != null);
     assert(showWhenUnlinked != null);
     if (link.leader == null && !showWhenUnlinked) {
       _lastTransform = null;
       _lastOffset = null;
       _inverseDirty = true;
-      return;
+      return null; // this does not have an engine layer.
     }
     _establishTransform();
     if (_lastTransform != null) {
@@ -1371,6 +1665,7 @@
       builder.pop();
     }
     _inverseDirty = true;
+    return null; // this does not have an engine layer.
   }
 
   @override
diff --git a/packages/flutter/lib/src/rendering/object.dart b/packages/flutter/lib/src/rendering/object.dart
index 8a92f06..3b53308 100644
--- a/packages/flutter/lib/src/rendering/object.dart
+++ b/packages/flutter/lib/src/rendering/object.dart
@@ -198,6 +198,7 @@
         return true;
       }());
     }
+    assert(child._layer != null);
     child._layer.offset = offset;
     appendLayer(child._layer);
   }
@@ -488,7 +489,7 @@
   /// ancestor render objects that this render object will include a composited
   /// layer, which, for example, causes them to use composited clips.
   void pushOpacity(Offset offset, int alpha, PaintingContextCallback painter) {
-    pushLayer(OpacityLayer(alpha: alpha), painter, offset);
+    pushLayer(OpacityLayer(alpha: alpha, offset: offset), painter, Offset.zero);
   }
 
   @override
diff --git a/packages/flutter/lib/src/rendering/view.dart b/packages/flutter/lib/src/rendering/view.dart
index 44e11b2..3719760 100644
--- a/packages/flutter/lib/src/rendering/view.dart
+++ b/packages/flutter/lib/src/rendering/view.dart
@@ -192,8 +192,7 @@
     Timeline.startSync('Compositing', arguments: timelineWhitelistArguments);
     try {
       final ui.SceneBuilder builder = ui.SceneBuilder();
-      layer.addToScene(builder);
-      final ui.Scene scene = builder.build();
+      final ui.Scene scene = layer.buildScene(builder);
       if (automaticSystemUiAdjustment)
         _updateSystemChrome();
       ui.window.render(scene);
diff --git a/packages/flutter/lib/src/widgets/widget_inspector.dart b/packages/flutter/lib/src/widgets/widget_inspector.dart
index 19dec90..673616c 100644
--- a/packages/flutter/lib/src/widgets/widget_inspector.dart
+++ b/packages/flutter/lib/src/widgets/widget_inspector.dart
@@ -12,6 +12,7 @@
     show
         window,
         ClipOp,
+        EngineLayer,
         Image,
         ImageByteFormat,
         Paragraph,
@@ -54,8 +55,8 @@
   final Layer _layer;
 
   @override
-  void addToScene(ui.SceneBuilder builder, [Offset layerOffset = Offset.zero]) {
-    _layer.addToScene(builder, layerOffset);
+  ui.EngineLayer addToScene(ui.SceneBuilder builder, [Offset layerOffset = Offset.zero]) {
+    return _layer.addToScene(builder, layerOffset);
   }
 
   @override
@@ -312,8 +313,9 @@
 /// screenshots render to the scene in the local coordinate system of the layer.
 class _ScreenshotContainerLayer extends OffsetLayer {
   @override
-  void addToScene(ui.SceneBuilder builder, [Offset layerOffset = Offset.zero]) {
+  ui.EngineLayer addToScene(ui.SceneBuilder builder, [Offset layerOffset = Offset.zero]) {
     addChildrenToScene(builder, layerOffset);
+    return null; // this does not have an engine layer.
   }
 }
 
@@ -588,7 +590,7 @@
     // We must build the regular scene before we can build the screenshot
     // scene as building the screenshot scene assumes addToScene has already
     // been called successfully for all layers in the regular scene.
-    repaintBoundary.layer.addToScene(ui.SceneBuilder());
+    repaintBoundary.layer.buildScene(ui.SceneBuilder());
 
     return data.containerLayer.toImage(renderBounds, pixelRatio: pixelRatio);
   }
@@ -2226,9 +2228,9 @@
   double _textPainterMaxWidth;
 
   @override
-  void addToScene(ui.SceneBuilder builder, [Offset layerOffset = Offset.zero]) {
+  ui.EngineLayer addToScene(ui.SceneBuilder builder, [Offset layerOffset = Offset.zero]) {
     if (!selection.active)
-      return;
+      return null;
 
     final RenderObject selected = selection.current;
     final List<_TransformedRect> candidates = <_TransformedRect>[];
@@ -2251,6 +2253,7 @@
       _picture = _buildPicture(state);
     }
     builder.addPicture(layerOffset, _picture);
+    return null; // this does not have an engine layer.
   }
 
   ui.Picture _buildPicture(_InspectorOverlayRenderState state) {
diff --git a/packages/flutter/test/rendering/layers_test.dart b/packages/flutter/test/rendering/layers_test.dart
index 727c655..0ce8f33 100644
--- a/packages/flutter/test/rendering/layers_test.dart
+++ b/packages/flutter/test/rendering/layers_test.dart
@@ -40,4 +40,73 @@
     expect(boundary.layer, isNotNull);
     expect(boundary.layer.attached, isTrue); // this time it did again!
   });
+
+  test('layer subtree dirtiness is correctly computed', () {
+    final ContainerLayer a = ContainerLayer();
+    final ContainerLayer b = ContainerLayer();
+    final ContainerLayer c = ContainerLayer();
+    final ContainerLayer d = ContainerLayer();
+    final ContainerLayer e = ContainerLayer();
+    final ContainerLayer f = ContainerLayer();
+    final ContainerLayer g = ContainerLayer();
+
+    final PictureLayer h = PictureLayer(Rect.zero);
+    final PictureLayer i = PictureLayer(Rect.zero);
+    final PictureLayer j = PictureLayer(Rect.zero);
+
+    // The tree is like the following where b and j are dirty:
+    //        a____
+    //       /     \
+    //   (x)b___    c
+    //     / \  \   |
+    //    d   e  f  g
+    //   / \        |
+    //  h   i       j(x)
+    a.append(b);
+    a.append(c);
+    b.append(d);
+    b.append(e);
+    b.append(f);
+    d.append(h);
+    d.append(i);
+    c.append(g);
+    g.append(j);
+
+    a.debugMarkClean();
+    b.markNeedsAddToScene();  // ignore: invalid_use_of_protected_member
+    c.debugMarkClean();
+    d.debugMarkClean();
+    e.debugMarkClean();
+    f.debugMarkClean();
+    g.debugMarkClean();
+    h.debugMarkClean();
+    i.debugMarkClean();
+    j.markNeedsAddToScene();  // ignore: invalid_use_of_protected_member
+
+    a.updateSubtreeNeedsAddToScene();
+
+    expect(a.debugSubtreeNeedsAddToScene, true);
+    expect(b.debugSubtreeNeedsAddToScene, true);
+    expect(c.debugSubtreeNeedsAddToScene, true);
+    expect(g.debugSubtreeNeedsAddToScene, true);
+    expect(j.debugSubtreeNeedsAddToScene, true);
+
+    expect(d.debugSubtreeNeedsAddToScene, false);
+    expect(e.debugSubtreeNeedsAddToScene, false);
+    expect(f.debugSubtreeNeedsAddToScene, false);
+    expect(h.debugSubtreeNeedsAddToScene, false);
+    expect(i.debugSubtreeNeedsAddToScene, false);
+  });
+
+  test('leader and follower layers are always dirty', () {
+    final LayerLink link = LayerLink();
+    final LeaderLayer leaderLayer = LeaderLayer(link: link);
+    final FollowerLayer followerLayer = FollowerLayer(link: link);
+    leaderLayer.debugMarkClean();
+    followerLayer.debugMarkClean();
+    leaderLayer.updateSubtreeNeedsAddToScene();
+    followerLayer.updateSubtreeNeedsAddToScene();
+    expect(leaderLayer.debugSubtreeNeedsAddToScene, true);
+    expect(followerLayer.debugSubtreeNeedsAddToScene, true);
+  });
 }