// Copyright 2016 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:math' as math; import 'package:flutter/rendering.dart'; import 'basic.dart'; import 'clamp_overscrolls.dart'; import 'framework.dart'; import 'scroll_configuration.dart'; import 'scrollable.dart'; import 'scrollable_list.dart'; import 'scroll_behavior.dart'; /// Provides children for [LazyBlock] or [LazyBlockViewport]. /// /// See also [LazyBlockBuilder] for an implementation of LazyBlockDelegate based /// on an [IndexedWidgetBuilder] closure. abstract class LazyBlockDelegate { /// Abstract const constructor. This constructor enables subclasses to provide /// const constructors so that they can be used in const expressions. const LazyBlockDelegate(); /// Returns a widget representing the item with the given index. /// /// This function might be called with index parameters in any order. This /// function should return null for indices that exceed the number of children /// provided by this delegate. If this function must not return a null value /// for an index if it previously returned a non-null value for that index or /// a larger index. /// /// This function might be called during the build or layout phases of the /// pipeline. /// /// The returned widget might or might not be cached by [LazyBlock]. See /// [shouldRebuild] for details about how to evict the cache. Widget buildItem(BuildContext context, int index); /// Whether [LazyBlock] should evict its cache of widgets returned by [buildItem]. /// /// When a [LazyBlock] receives a new configuration with a new delegate, it /// evicts its cache of widgets if (1) the new configuration has a delegate /// with a different runtimeType than the old delegate, or (2) the /// [shouldRebuild] method of the new delegate returns true when passed the /// old delgate. /// /// When calling this function, [LazyBlock] will always pass an argument that /// matches the runtimeType of the receiver. bool shouldRebuild(LazyBlockDelegate oldDelegate); } /// Uses an [IndexedWidgetBuilder] to provide children for [LazyBlock]. /// /// A LazyBlockBuilder rebuilds the children whenever the [LazyBlock] is /// rebuilt, similar to the behavior of [Builder]. /// /// See also [LazyBlockViewport]. class LazyBlockBuilder extends LazyBlockDelegate { /// Creates a LazyBlockBuilder based on the given builder. LazyBlockBuilder({ this.builder }) { assert(builder != null); } /// Returns a widget representing the item with the given index. /// /// This function might be called with index parameters in any order. This /// function should return null for indices that exceed the number of children /// provided by this delegate. This function must not return a null value /// for an index if it previously returned a non-null value for that index or /// a larger index. /// /// This function might be called during the build or layout phases of the /// pipeline. final IndexedWidgetBuilder builder; @override Widget buildItem(BuildContext context, int index) => builder(context, index); @override bool shouldRebuild(LazyBlockDelegate oldDelegate) => true; } /// Uses a [List<Widget>] to provide children for [LazyBlock]. /// /// See also [LazyBlockViewport]. class LazyBlockChildren extends LazyBlockDelegate { /// Creates a LazyBlockChildren that displays the given children. /// /// The list of children must not be modified after being passed to this /// constructor. LazyBlockChildren({ this.children }) { assert(children != null); } /// The widgets to display. /// /// This list must not be modified after being stored in this field. final List<Widget> children; @override Widget buildItem(BuildContext context, int index) { assert(index >= 0); return index < children.length ? children[index] : null; } @override bool shouldRebuild(LazyBlockChildren oldDelegate) { return children != oldDelegate.children; } } /// An infinite scrolling list of variable height children. /// /// [LazyBlock] is a general-purpose scrollable list for a large (or infinite) /// number of children that might not all have the same height. Rather than /// materializing all of its children, [LazyBlock] asks its [delegate] to build /// child widgets lazily to fill its viewport. [LazyBlock] caches the widgets /// it obtains from the delegate as long as they're visible. (See /// [LazyBlockDelegate.shouldRebuild] for details about how to evict the cache.) /// /// [LazyBlock] works by dead reckoning changes to its [scrollOffset] from the /// top of the first child that is visible in its viewport. If the children /// above the first visible child change size, the [scrollOffset] might not /// return to zero when the [LazyBlock] is scrolled all the way back to the /// start because the height of each child will be subtracted incrementally from /// the current scroll position. For this reason, making large changes to the /// [scrollOffset] is expensive because [LazyBlock] computes the size of every /// child between the old scroll offset and the new scroll offset. /// /// Prefer [ScrollableList] when all the children have the same height because /// it can use that property to be more efficient. Prefer [ScrollableViewport] /// when there is only one child. class LazyBlock extends StatelessWidget { LazyBlock({ Key key, this.initialScrollOffset, this.scrollDirection: Axis.vertical, this.onScrollStart, this.onScroll, this.onScrollEnd, this.snapOffsetCallback, this.scrollableKey, this.padding, this.delegate }) : super(key: key); // Warning: keep the dartdoc comments that follow in sync with the copies in // Scrollable, ScrollableGrid, ScrollableViewport, ScrollableList, and // ScrollableLazyList. And see: https://github.com/dart-lang/dartdoc/issues/1161. /// The scroll offset this widget should use when first created. final double initialScrollOffset; /// The axis along which this widget should scroll. final Axis scrollDirection; /// Called whenever this widget starts to scroll. final ScrollListener onScrollStart; /// Called whenever this widget's scroll offset changes. final ScrollListener onScroll; /// Called whenever this widget stops scrolling. final ScrollListener onScrollEnd; /// Called to determine the offset to which scrolling should snap, /// when handling a fling. /// /// This callback, if set, will be called with the offset that the /// Scrollable would have scrolled to in the absence of this /// callback, and a Size describing the size of the Scrollable /// itself. /// /// The callback's return value is used as the new scroll offset to /// aim for. /// /// If the callback simply returns its first argument (the offset), /// then it is as if the callback was null. final SnapOffsetCallback snapOffsetCallback; /// The key for the Scrollable created by this widget. final Key scrollableKey; /// The amount of space by which to inset the children inside the viewport. final EdgeInsets padding; /// Provides children for this widget. /// /// See [LazyBlockDelegate] for details. final LazyBlockDelegate delegate; void _handleExtentsChanged( ScrollableState state, double contentExtent, double containerExtent, double minScrollOffset) { state.setState(() { final BoundedBehavior scrollBehavior = state.scrollBehavior; state.didUpdateScrollBehavior(scrollBehavior.updateExtents( contentExtent: contentExtent, containerExtent: containerExtent, minScrollOffset: minScrollOffset, scrollOffset: state.scrollOffset )); }); } Widget _buildViewport(BuildContext context, ScrollableState state, double scrollOffset) { return new LazyBlockViewport( startOffset: scrollOffset, mainAxis: scrollDirection, padding: padding, onExtentsChanged: (double contentExtent, double containerExtent, double minScrollOffset) { _handleExtentsChanged(state, contentExtent, containerExtent, minScrollOffset); }, delegate: delegate ); } Widget _buildContent(BuildContext context, ScrollableState state) { return ClampOverscrolls.buildViewport(context, state, _buildViewport); } @override Widget build(BuildContext context) { final Widget result = new Scrollable( key: scrollableKey, initialScrollOffset: initialScrollOffset, scrollDirection: scrollDirection, onScrollStart: onScrollStart, onScroll: onScroll, onScrollEnd: onScrollEnd, snapOffsetCallback: snapOffsetCallback, builder: _buildContent ); return ScrollConfiguration.wrap(context, result); } } /// Signature used by [LazyBlockViewport] to report its interior and exterior dimensions. /// /// * The [contentExtent] is the interior dimension of the viewport (i.e., the /// size of the thing that's being viewed through the viewport). /// * The [containerExtent] is the exterior dimension of the viewport (i.e., /// the amount of the thing inside the viewport that is visible from outside /// the viewport). /// * The [minScrollOffset] is the offset at which the starting edge of the /// first item in the viewport is aligned with the starting edge of the /// viewport. (As the scroll offset increases, items with larger indices are /// revealed in the viewport.) Typically the min scroll offset is 0.0, but /// because [LazyBlockViewport] uses dead reckoning, the min scroll offset /// might not always be 0.0. For example, if an item that's offscreen changes /// size, the visible items will retain their current scroll offsets even if /// the distance to the starting edge of the first item changes. typedef void LazyBlockExtentsChangedCallback(double contentExtent, double containerExtent, double minScrollOffset); /// A viewport on an infinite list of variable height children. /// /// [LazyBlockViewport] is a a general-purpose viewport for a large (or /// infinite) number of children that might not all have the same height. Rather /// than materializing all of its children, [LazyBlockViewport] asks its /// [delegate] to build child widgets lazily to fill itself. [LazyBlockViewport] /// caches the widgets it obtains from the delegate as long as they're visible. /// (See [LazyBlockDelegate.shouldRebuild] for details about how to evict the /// cache.) /// /// [LazyBlockViewport] works by dead reckoning changes to its [startOffset] /// from the top of the first child that is visible in itself. For this reason, /// making large changes to the [startOffset] is expensive because /// [LazyBlockViewport] computes the size of every child between the old offset /// and the new offset. /// /// Prefer [ListViewport] when all the children have the same height because /// it can use that property to be more efficient. Prefer [Viewport] when there /// is only one child. /// /// For a scrollable version of this widget, see [LazyBlock]. class LazyBlockViewport extends RenderObjectWidget { LazyBlockViewport({ Key key, this.startOffset: 0.0, this.mainAxis: Axis.vertical, this.padding, this.onExtentsChanged, this.delegate }) : super(key: key) { assert(delegate != null); } /// The offset of the start of the viewport. /// /// As the start offset increases, children with larger indices are visible /// in the viewport. /// /// For vertical viewports, the offset is from the top of the viewport. For /// horizontal viewports, the offset is from the left of the viewport. final double startOffset; /// The direction in which the children are permitted to be larger than the viewport /// /// The children are given layout constraints that are fully unconstrainted /// along the main axis (e.g., children can be as tall as it wants if the main /// axis is vertical). final Axis mainAxis; /// The amount of space by which to inset the children inside the viewport. final EdgeInsets padding; /// Called when the interior or exterior dimensions of the viewport change. final LazyBlockExtentsChangedCallback onExtentsChanged; /// Provides children for this widget. /// /// See [LazyBlockDelegate] for details. final LazyBlockDelegate delegate; double get _mainAxisPadding { if (padding == null) return 0.0; switch (mainAxis) { case Axis.horizontal: return padding.horizontal; case Axis.vertical: return padding.vertical; } } @override _LazyBlockElement createElement() => new _LazyBlockElement(this); @override _RenderLazyBlock createRenderObject(BuildContext context) => new _RenderLazyBlock(); } class _LazyBlockParentData extends ContainerBoxParentDataMixin<RenderBox> { } class _RenderLazyBlock extends RenderVirtualViewport<_LazyBlockParentData> { _RenderLazyBlock({ Offset paintOffset: Offset.zero, Axis mainAxis: Axis.vertical, LayoutCallback callback }) : super( paintOffset: paintOffset, mainAxis: mainAxis, callback: callback ); @override void setupParentData(RenderBox child) { if (child.parentData is! _LazyBlockParentData) child.parentData = new _LazyBlockParentData(); } bool _debugThrowIfNotCheckingIntrinsics() { assert(() { if (!RenderObject.debugCheckingIntrinsics) { throw new FlutterError( 'LazyBlockViewport does not support returning intrinsic dimensions.\n' 'Calculating the intrinsic dimensions would require walking the entire ' 'child list, which defeats the entire point of having a lazily-built ' 'list of children.' ); } return true; }); return true; } double getIntrinsicWidth(BoxConstraints constraints) { switch (mainAxis) { case Axis.horizontal: return constraints.constrainWidth(0.0); case Axis.vertical: assert(_debugThrowIfNotCheckingIntrinsics()); return constraints.constrainWidth(0.0); } } @override double getMinIntrinsicWidth(BoxConstraints constraints) { assert(constraints.debugAssertIsValid()); return getIntrinsicWidth(constraints); } @override double getMaxIntrinsicWidth(BoxConstraints constraints) { assert(constraints.debugAssertIsValid()); return getIntrinsicWidth(constraints); } double getIntrinsicHeight(BoxConstraints constraints) { switch (mainAxis) { case Axis.horizontal: return constraints.constrainHeight(0.0); case Axis.vertical: assert(_debugThrowIfNotCheckingIntrinsics()); return constraints.constrainHeight(0.0); } } @override double getMinIntrinsicHeight(BoxConstraints constraints) { assert(constraints.debugAssertIsValid()); return getIntrinsicHeight(constraints); } @override double getMaxIntrinsicHeight(BoxConstraints constraints) { assert(constraints.debugAssertIsValid()); return getIntrinsicHeight(constraints); } @override bool get sizedByParent => true; @override bool get isRepaintBoundary => true; @override void performResize() { size = constraints.biggest; } @override void performLayout() { if (callback != null) invokeLayoutCallback(callback); } } class _LazyBlockElement extends RenderObjectElement { _LazyBlockElement(LazyBlockViewport widget) : super(widget); @override LazyBlockViewport get widget => super.widget; @override _RenderLazyBlock get renderObject => super.renderObject; /// The offset of the top of the first item represented in _children from the top of the item with logical index zero. double _firstChildLogicalOffset = 0.0; /// The logical index of the first item represented in _children. int _firstChildLogicalIndex = 0; /// The explicitly represented items. List<Element> _children = <Element>[]; /// The minimum scroll offset used by the scroll behavior. /// /// Not all the items between the minimum and maximum scroll offsets are /// reprsented explicitly in _children. double _minScrollOffset = 0.0; /// The maximum scroll offset used by the scroll behavior. /// /// Not all the items between the minimum and maximum scroll offsets are /// reprsented explicitly in _children. double _maxScrollOffset = 0.0; /// The smallest start offset (inclusive) that can be displayed properly with the items currently represented in [_children]. double _startOffsetLowerLimit = 0.0; /// The largest start offset (exclusive) that can be displayed properly with the items currently represented in [_children]. double _startOffsetUpperLimit = 0.0; double _lastReportedContentExtent; double _lastReportedContainerExtent; double _lastReportedMinScrollOffset; @override void visitChildren(ElementVisitor visitor) { for (Element child in _children) visitor(child); } @override void mount(Element parent, dynamic newSlot) { super.mount(parent, newSlot); renderObject ..callback = _layout ..mainAxis = widget.mainAxis; // Children will get built during layout. // Paint offset will get updated during layout. } @override void update(LazyBlockViewport newWidget) { LazyBlockViewport oldWidget = widget; super.update(newWidget); renderObject.mainAxis = widget.mainAxis; LazyBlockDelegate newDelegate = newWidget.delegate; LazyBlockDelegate oldDelegate = oldWidget.delegate; if (newDelegate != oldDelegate && (newDelegate.runtimeType != oldDelegate.runtimeType || newDelegate.shouldRebuild(oldDelegate))) performRebuild(); // If the new start offset can be displayed properly with the items // currently represented in _children, we just need to update the paint // offset. Otherwise, we need to trigger a layout in order to change the // set of explicitly represented children. double startOffset = widget.startOffset; if (startOffset >= _startOffsetLowerLimit && startOffset < _startOffsetUpperLimit && newWidget.padding == oldWidget.padding) { _updatePaintOffset(); } else { renderObject.markNeedsLayout(); } } @override void unmount() { renderObject.callback = null; super.unmount(); } @override void performRebuild() { IndexedWidgetBuilder builder = widget.delegate.buildItem; List<Widget> widgets = <Widget>[]; for (int i = 0; i < _children.length; ++i) { int logicalIndex = _firstChildLogicalIndex + i; Widget childWidget = builder(this, logicalIndex); if (childWidget == null) break; widgets.add(new RepaintBoundary.wrap(childWidget, logicalIndex)); } _children = new List<Element>.from(updateChildren(_children, widgets)); super.performRebuild(); } void _layout(BoxConstraints constraints) { final double blockExtent = _getMainAxisExtent(renderObject.size); final IndexedWidgetBuilder builder = widget.delegate.buildItem; final double startLogicalOffset = widget.startOffset; final double endLogicalOffset = startLogicalOffset + blockExtent; final _RenderLazyBlock block = renderObject; final BoxConstraints innerConstraints = _getInnerConstraints(constraints); // A high watermark for which children have been through layout this pass. int firstLogicalIndexNeedingLayout = _firstChildLogicalIndex; // The index of the current child we're examining. The index is the same one // used for the builder (as opposed to the physical index in the _children // list). int currentLogicalIndex = _firstChildLogicalIndex; // The offset of the current child we're examining from the start of the // entire block (in the direction of the main axis). As we compute layout // information, we use dead reckoning to keep track of where all the // children are based on this quantity. double currentLogicalOffset = _firstChildLogicalOffset; // First, we check if we need to inflate any children before the start of // the viewport. Because we're dead reckoning from the current viewport, we // inflate the children in reverse tree order. if (currentLogicalIndex > 0 && currentLogicalOffset > startLogicalOffset) { final List<Element> newChildren = <Element>[]; while (currentLogicalIndex > 0 && currentLogicalOffset > startLogicalOffset) { currentLogicalIndex -= 1; Element newElement; owner.lockState(() { // TODO(abarth): Handle exceptions from builder gracefully. Widget newWidget = builder(this, currentLogicalIndex); if (newWidget == null) { throw new FlutterError( 'buildItem must not return null after returning non-null.\n' 'If buildItem for a LazyBlockDelegate returns a non-null widget for a given ' 'index, it must return non-null widgets for every smaller index as well. The ' 'buildItem function for ${widget.delegate.runtimeType} returned null for ' 'index $currentLogicalIndex after having returned a non-null value for index ' '${currentLogicalIndex - 1}.' ); } newWidget = new RepaintBoundary.wrap(newWidget, currentLogicalIndex); newElement = inflateWidget(newWidget, null); }, building: true); newChildren.add(newElement); RenderBox child = block.firstChild; assert(child == newChildren.last.renderObject); child.layout(innerConstraints, parentUsesSize: true); currentLogicalOffset -= _getMainAxisExtent(child.size); } final int numberOfNewChildren = newChildren.length; _children.insertAll(0, newChildren.reversed); _firstChildLogicalIndex = currentLogicalIndex; _firstChildLogicalOffset = currentLogicalOffset; firstLogicalIndexNeedingLayout = currentLogicalIndex + numberOfNewChildren; } else if (currentLogicalOffset < startLogicalOffset) { // If we didn't need to inflate more children before the viewport, we // might need to deactivate children that have left the viewport from the // top. We repeatedly check whether the first child overlaps the viewport // and deactivate it if it's outside the viewport. int currentPhysicalIndex = 0; while (block.firstChild != null) { RenderBox child = block.firstChild; child.layout(innerConstraints, parentUsesSize: true); firstLogicalIndexNeedingLayout += 1; double childExtent = _getMainAxisExtent(child.size); if (currentLogicalOffset + childExtent >= startLogicalOffset) break; deactivateChild(_children[currentPhysicalIndex]); _children[currentPhysicalIndex] = null; currentPhysicalIndex += 1; currentLogicalIndex += 1; currentLogicalOffset += childExtent; } if (currentPhysicalIndex > 0) { _children.removeRange(0, currentPhysicalIndex); _firstChildLogicalIndex = currentLogicalIndex; _firstChildLogicalOffset = currentLogicalOffset; } } // We've now established the invariant that the first physical child in the // block is the first child that ought to be visible in the viewport. Now we // need to walk forward until we've filled up the viewport. We might have // already called layout for some of the children we encounter in this phase // of the algorithm, we we'll need to be careful not to call layout on them again. if (currentLogicalOffset >= startLogicalOffset) { // The first element is visible. We need to update our reckoning of where // the min scroll offset is. _minScrollOffset = currentLogicalOffset; _startOffsetLowerLimit = double.NEGATIVE_INFINITY; } else { // The first element is not visible. Ensure that we have one blockExtent // of headroom so we don't hit the min scroll offset prematurely. _minScrollOffset = currentLogicalOffset - blockExtent; _startOffsetLowerLimit = currentLogicalOffset; } // Materialize new children until we fill the viewport (or run out of // children to materialize). RenderBox child; while (currentLogicalOffset < endLogicalOffset) { int physicalIndex = currentLogicalIndex - _firstChildLogicalIndex; if (physicalIndex >= _children.length) { assert(physicalIndex == _children.length); Element newElement; owner.lockState(() { // TODO(abarth): Handle exceptions from builder gracefully. Widget newWidget = builder(this, currentLogicalIndex); if (newWidget == null) return; newWidget = new RepaintBoundary.wrap(newWidget, currentLogicalIndex); Element previousChild = _children.isEmpty ? null : _children.last; newElement = inflateWidget(newWidget, previousChild); }, building: true); if (newElement == null) break; _children.add(newElement); } child = _getNextWithin(block, child); assert(child != null); if (currentLogicalIndex >= firstLogicalIndexNeedingLayout) { assert(currentLogicalIndex == firstLogicalIndexNeedingLayout); child.layout(innerConstraints, parentUsesSize: true); firstLogicalIndexNeedingLayout += 1; } currentLogicalOffset += _getMainAxisExtent(child.size); currentLogicalIndex += 1; } // We now have all the physical children we ought to have to fill the // viewport. The currentLogicalIndex is the index of the first child that // we don't need. if (currentLogicalOffset < endLogicalOffset) { // The last element is visible. We need to update our reckoning of where // the max scroll offset is. _maxScrollOffset = currentLogicalOffset + widget._mainAxisPadding - blockExtent; _startOffsetUpperLimit = double.INFINITY; } else { // The last element is not visible. Ensure that we have one blockExtent // of headroom so we don't hit the max scroll offset prematurely. _maxScrollOffset = currentLogicalOffset; _startOffsetUpperLimit = currentLogicalOffset - blockExtent; } // Remove any unneeded children. int currentPhysicalIndex = currentLogicalIndex - _firstChildLogicalIndex; final int numberOfRequiredPhysicalChildren = currentPhysicalIndex; while (currentPhysicalIndex < _children.length) { deactivateChild(_children[currentPhysicalIndex]); _children[currentPhysicalIndex] = null; currentPhysicalIndex += 1; } _children.length = numberOfRequiredPhysicalChildren; // We now have the correct physical children, each of which has gone through // layout exactly once. We still need to position them correctly. We // position the first physical child at Offset.zero and use the paintOffset // on the render object to adjust the final paint location of the children. Offset currentChildOffset = _initialChildOffset; child = block.firstChild; while (child != null) { final _LazyBlockParentData childParentData = child.parentData; childParentData.offset = currentChildOffset; currentChildOffset += _getMainAxisOffsetForSize(child.size); child = childParentData.nextSibling; } _updatePaintOffset(); LazyBlockExtentsChangedCallback onExtentsChanged = widget.onExtentsChanged; if (onExtentsChanged != null) { double contentExtent = _maxScrollOffset - _minScrollOffset + blockExtent; if (_lastReportedContentExtent != contentExtent || _lastReportedContainerExtent != blockExtent || _lastReportedMinScrollOffset != _minScrollOffset) { _lastReportedContentExtent = contentExtent; _lastReportedContainerExtent = blockExtent; _lastReportedMinScrollOffset = _minScrollOffset; onExtentsChanged(_lastReportedContentExtent, _lastReportedContainerExtent, _lastReportedMinScrollOffset); } } } BoxConstraints _getInnerConstraints(BoxConstraints constraints) { switch (widget.mainAxis) { case Axis.horizontal: double padding = widget.padding?.vertical ?? 0.0; double height = math.max(0.0, constraints.maxHeight - padding); return new BoxConstraints.tightFor(height: height); case Axis.vertical: double padding = widget.padding?.horizontal ?? 0.0; double width = math.max(0.0, constraints.maxWidth - padding); return new BoxConstraints.tightFor(width: width); } } Offset get _initialChildOffset { if (widget.padding == null) return Offset.zero; return new Offset(widget.padding.left, widget.padding.top); } double _getMainAxisExtent(Size size) { switch (widget.mainAxis) { case Axis.horizontal: return size.width; case Axis.vertical: return size.height; } } Offset _getMainAxisOffsetForSize(Size size) { switch (widget.mainAxis) { case Axis.horizontal: return new Offset(size.width, 0.0); case Axis.vertical: return new Offset(0.0, size.height); } } static RenderBox _getNextWithin(_RenderLazyBlock block, RenderBox child) { if (child == null) return block.firstChild; final _LazyBlockParentData childParentData = child.parentData; return childParentData.nextSibling; } void _updatePaintOffset() { double physicalStartOffset = widget.startOffset - _firstChildLogicalOffset; switch (widget.mainAxis) { case Axis.horizontal: renderObject.paintOffset = new Offset(-physicalStartOffset, 0.0); break; case Axis.vertical: renderObject.paintOffset = new Offset(0.0, -physicalStartOffset); break; } } @override void insertChildRenderObject(RenderObject child, Element slot) { renderObject.insert(child, after: slot?.renderObject); } @override void moveChildRenderObject(RenderObject child, dynamic slot) { assert(child.parent == renderObject); renderObject.move(child, after: slot?.renderObject); } @override void removeChildRenderObject(RenderObject child) { assert(child.parent == renderObject); renderObject.remove(child); } }