preface

Following on from the previous chapter, ScrollView, This chapter we will follow the ListView/GridView = > SliverList/SliverGrid = > RenderSliverList/RenderSliverGrid lines, carding list calculation the final a kilometer of the code, for the N.

Welcome to Flutter CandiesQQ group: 181398081

  • Flutter Silver’s Lifetime Enemy (ScrollView)
  • Flutter silver lifetime enemies (ExtendedList)
  • The Flutter Silver Waterfall flow
  • Flutter silver locks your beauty

Layout inputs and outputs for Silver

Before looking at the layout code, it’s important to look at the inputs and outputs of the Silver layout

SliverConstraints

The input to the Sliver layout is the constraint that the Viewport tells us.

class SliverConstraints extends Constraints {
  /// Creates sliver constraints with the given information.
  ///
  /// All of the argument must not be null.
  const SliverConstraints({
    // Roll direction
    @required this.axisDirection,
    // This is for center. Sliver before center is upside down
    @required this.growthDirection,
    // The direction of the user's gesture
    @required this.userScrollDirection,
    // The offset for the scroll. Note that this is for this Sliver and not the total offset for the entire Slivers
    @required this.scrollOffset,
    // The total size of the preceding Slivers
    @required this.precedingScrollExtent,
    // Designed for pinned and floating. If the previous Sliver draws a size of 100 but the layout size is only 50, overlap of this Sliver is 50.
    @required this.overlap,
    // How much more can be drawn? Refer to viewport and cache. For example, if the number of Slivers is 100, then the size of the remaining area should be subtracted from the size of the previous area
    @required this.remainingPaintExtent,
    // The size of the vertical axis
    @required this.crossAxisExtent,
    // The direction of the vertical axis, which affects the order of the elements in the same row in the GridView, is 0~x or x~0
    @required this.crossAxisDirection,
    // How much content can be drawn from the viewport
    @required this.viewportMainAxisExtent,
    // The size of the remaining cache area
    @required this.remainingCacheExtent,
    // Cache area size relative to scrollOffset
    @required this.cacheOrigin,
  })
Copy the code

SliverGeometry

The output of the Sliver layout is fed back to the Viewport.

@immutable
class SliverGeometry extends Diagnosticable {
  /// Creates an object that describes the amount of space occupied by a sliver.
  ///
  /// If the [layoutExtent] argument is null, [layoutExtent] defaults to the
  /// [paintExtent]. If the [hitTestExtent] argument is null, [hitTestExtent]
  /// defaults to the [paintExtent]. If [visible] is null, [visible] defaults to
  /// whether [paintExtent] is greater than zero.
  ///
  /// The other arguments must not be null.
  const SliverGeometry({
    // The estimated size of the Sliver can scroll
    this.scrollExtent = 0.0./ / to influence after an overlap of attribute, it is less than [SliverConstraints. RemainingPaintExtent], for silver within the scope of the viewport (including cache) the size of the first element to the last element
    this.paintExtent = 0.0.// Draw the starting point relative to the Silver position
    this.paintOrigin = 0.0.// The size of this sliver from the first display location of the viewport to the first display location of the next sliver
    double layoutExtent,
    / / draw the total size of the largest, this parameter is used to [SliverConstraints. RemainingPaintExtent] is infinite, is to use in the shrink - wrapping in the viewport
    this.maxPaintExtent = 0.0.// If Sliver is pinned to the boundary, this size is the sliver's own height. Otherwise, it's 0
    this.maxScrollObstructionExtent = 0.0.// Click the size of the valid area, which defaults to paintExtent
    double hitTestExtent,
    // If paintExtent is 0, it is not visible.
    bool visible,
    // Do you need to do clip to prevent chidren overflow
    this.hasVisualOverflow = false.// The value of the viewport layout sliver will not be equal to zero if there is a problem with the sliver. Use this value to correct the entire ScrollOffset
    this.scrollOffsetCorrection,
    / / the silver from how much [SliverConstraints remainingCacheExtent], in the presence of many Slivers
    double cacheExtent,
  })
Copy the code

Roughly explained the meaning of these parameters, may still not understand, in the source code behind the use will also be explained according to the scene.

BoxScrollView

Widget Extends
ListView/GridView BoxScrollView => ScrollView

ListView and GirdView both inherit from BoxScrollView, so let’s see how BoxScrollView differs from ScrollView.

The key code

/// The amount of space by which to inset the children.
  final EdgeInsetsGeometry padding;

  @override
  List<Widget> buildSlivers(BuildContext context) {
    /// This method is implemented by ListView/GirdView
    Widget sliver = buildChildLayout(context);
    EdgeInsetsGeometry effectivePadding = padding;
    if (padding == null) {
      final MediaQueryData mediaQuery = MediaQuery.of(context, nullOk: true);
      if(mediaQuery ! =null) {
        // Automatically pad sliver with padding from MediaQuery.
        final EdgeInsets mediaQueryHorizontalPadding =
            mediaQuery.padding.copyWith(top: 0.0, bottom: 0.0);
        final EdgeInsets mediaQueryVerticalPadding =
            mediaQuery.padding.copyWith(left: 0.0, right: 0.0);
        // Consume the main axis padding with SliverPadding.
        effectivePadding = scrollDirection == Axis.vertical
            ? mediaQueryVerticalPadding
            : mediaQueryHorizontalPadding;
        // Leave behind the cross axis padding.sliver = MediaQuery( data: mediaQuery.copyWith( padding: scrollDirection == Axis.vertical ? mediaQueryHorizontalPadding : mediaQueryVerticalPadding, ), child: sliver, ); }}if(effectivePadding ! =null)
      sliver = SliverPadding(padding: effectivePadding, sliver: sliver);
    return <Widget>[ sliver ];
  }

  /// Subclasses should override this method to build the layout model.
  @protected
  /// This method is implemented by ListView/GirdView
  Widget buildChildLayout(BuildContext context);
Copy the code

As you can see, it’s just an extra layer of SliverPadding, and the returned [sliver] also shows that the ListView and GridView are actually a single sliver compared to the CustomScrollView, which can be multiple Slivers.

ListView

The key code

BuildChildLayout is called in the BoxScrollView’s buildSlivers method, which is implemented in the ListView. SliverList and SliverFixedExtentList are returned based on itemExtent.

  @override
  Widget buildChildLayout(BuildContext context) {
    if(itemExtent ! =null) {
      return SliverFixedExtentList(
        delegate: childrenDelegate,
        itemExtent: itemExtent,
      );
    }
    return SliverList(delegate: childrenDelegate);
  }
Copy the code

SliverList

class SliverList extends SliverMultiBoxAdaptorWidget {
  /// Creates a sliver that places box children in a linear array.
  const SliverList({
    Key key,
    @required SliverChildDelegate delegate,
  }) : super(key: key, delegate: delegate);

  @override
  RenderSliverList createRenderObject(BuildContext context) {
    final SliverMultiBoxAdaptorElement element = context;
    returnRenderSliverList(childManager: element); }}Copy the code

RenderSliverList

Silver layout

PerformLayout in RenderSliverList (github.com/flutter/flu…

The green is what we can see, the yellow is the cache area, and the gray is what should be reclaimed.

  • Layout ready to start
    // Indicate start
    childManager.didStartLayout();
    // Indicates whether a new child can be added
    childManager.setDidUnderflow(false);
    
    // Constraints are the layout constraints given by the viewport
    // The scroll position contains the cache, the starting position of the layout area
    final double scrollOffset = constraints.scrollOffset + constraints.cacheOrigin;
    assert(scrollOffset >= 0.0);
    // Draw the entire area size including the cache area, which is the yellow and green parts of the figure
    final double remainingExtent = constraints.remainingCacheExtent;
    assert(remainingExtent >= 0.0);
    // Layout the end of the area
    final double targetEndScrollOffset = scrollOffset + remainingExtent;
    // Get the limit of the child, if the list is scrolling vertically, the height should be infinite double. Infinity
    final BoxConstraints childConstraints = constraints.asBoxConstraints();
    // The number of children to recycle from the first child, shown in gray
    int leadingGarbage = 0;
    // The number of children to recycle from the last child forward, shown in gray
    int trailingGarbage = 0;
    // Whether to scroll to the end
    bool reachedEnd = false;
    
    // If there is no child in the list, we will try to add one. If this fails, the entire Sliver will have no content
    if (firstChild == null) {
      if(! addInitialChild()) {// There are no children.
        geometry = SliverGeometry.zero;
        childManager.didFinishLayout();
        return; }}Copy the code
  • In the case of forward computation, (a vertically scrolling list) the list wants to scroll forward. Since the gray child will be removed, when we scroll forward, we need to see if we need to insert the child in front of us based on where we are scrolling.
    // Find the last child that is at or before the scrollOffset.
    RenderBox earliestUsefulChild = firstChild;
    // When the first child's layoutOffset is less than our scrolling position, the front is empty, if the first child's signature is inserted with a new child to fill it
    for (double earliestScrollOffset =
    childScrollOffset(earliestUsefulChild);
        earliestScrollOffset > scrollOffset;
        earliestScrollOffset = childScrollOffset(earliestUsefulChild)) {
      // We have to add children before the earliestUsefulChild.
      // Insert the new child
      earliestUsefulChild = insertAndLayoutLeadingChild(childConstraints, parentUsesSize: true);
      // Handle when there is no child in the front
      if (earliestUsefulChild == null) {
        final SliverMultiBoxAdaptorParentData childParentData = firstChild.parentData as SliverMultiBoxAdaptorParentData;
        childParentData.layoutOffset = 0.0;
        
        // we're already at 0.0, so we don't need to go any further, break
        if (scrollOffset == 0.0) {
          // insertAndLayoutLeadingChild only lays out the children before
          // firstChild. In this case, nothing has been laid out. We have
          // to lay out firstChild manually.
          firstChild.layout(childConstraints, parentUsesSize: true); earliestUsefulChild = firstChild; leadingChildWithLayout = earliestUsefulChild; trailingChildWithLayout ?? = earliestUsefulChild;break;
        } else {
          // We ran out of children before reaching the scroll offset.
          // We must inform our parent that this sliver cannot fulfill
          // its contract and that we need a scroll offset correction.
          // This is what we talked about in the last chapter. Set the scrollOffsetCorrection to non-zero and pass it to the Viewport. This will remove the correction as a whole and rearrange the layout.
          geometry = SliverGeometry(
            scrollOffsetCorrection: -scrollOffset,
          );
          return; }}/// The scroll position subtracted the size of firstChild to continue to calculate whether more children need to be inserted to make up for it.
      final double firstChildScrollOffset = earliestScrollOffset - paintExtentOf(firstChild);
      // firstChildScrollOffset may contain double precision error
      // In the same way, if the final subtraction is less than 0.0(precisionErrorTolerance is a minimal number close to 0.0), it must be wrong, so tell the viewport to remove the difference and rearrange
      if (firstChildScrollOffset < -precisionErrorTolerance) {
        // The first child doesn't fit within the viewport (underflow) and
        // there may be additional children above it. Find the real first child
        // and then correct the scroll position so that there's room for all and
        // so that the trailing edge of the original firstChild appears where it
        // was before the scroll offset correction.
        // TODO(hansmuller): do this work incrementally, instead of all at once,
        // i.e. find a way to avoid visiting ALL of the children whose offset
        // is < 0 before returning for the scroll correction.
        double correction = 0.0;
        while(earliestUsefulChild ! =null) {
          assert(firstChild == earliestUsefulChild);
          correction += paintExtentOf(firstChild);
          earliestUsefulChild = insertAndLayoutLeadingChild(childConstraints, parentUsesSize: true);
        }
        geometry = SliverGeometry(
          scrollOffsetCorrection: correction - earliestScrollOffset,
        );
        final SliverMultiBoxAdaptorParentData childParentData = firstChild.parentData as SliverMultiBoxAdaptorParentData;
        childParentData.layoutOffset = 0.0;
        return;
      }
      // Ok, here is the normal situation
      final SliverMultiBoxAdaptorParentData childParentData = earliestUsefulChild.parentData as SliverMultiBoxAdaptorParentData;
      // Set the starting point for child drawing
      childParentData.layoutOffset = firstChildScrollOffset;
      assert(earliestUsefulChild == firstChild); leadingChildWithLayout = earliestUsefulChild; trailingChildWithLayout ?? = earliestUsefulChild; }Copy the code
  • Advance method (github.com/flutter/flu…).

Move the child backwards, return false if there is none left

    bool inLayoutRange = true;
    RenderBox child = earliestUsefulChild;
    int index = indexOf(child);
    double endScrollOffset = childScrollOffset(child) + paintExtentOf(child);
    bool advance() { // returns true if we advanced, false if we have no more children
      // This function is used in two different places below, to avoid code duplication.
      assert(child ! =null);
      if (child == trailingChildWithLayout)
        inLayoutRange = false;
      child = childAfter(child);
      ///It's not in Render Tree
      if (child == null)
        inLayoutRange = false;
      index += 1;
      if(! inLayoutRange) {if (child == null|| indexOf(child) ! = index) {// We are missing a child. Insert it (and lay it out) if possible.
          // It's not in the tree, try adding it
          child = insertAndLayoutChild(childConstraints,
            after: trailingChildWithLayout,
            parentUsesSize: true,);if (child == null) {
            // We have run out of children.
            return false; }}else {
          // Lay out the child.
          child.layout(childConstraints, parentUsesSize: true);
        }
        trailingChildWithLayout = child;
      }
      assert(child ! =null);
      final SliverMultiBoxAdaptorParentData childParentData = child.parentData as SliverMultiBoxAdaptorParentData;
      // Set the drawing position
      childParentData.layoutOffset = endScrollOffset;
      assert(childParentData.index == index);
      // Set endScrollOffset to the end of the drawing for the child
      endScrollOffset = childScrollOffset(child) + paintExtentOf(child);
      return true;
    }
Copy the code
  • Find the child closest to the scrollOffset

When we scroll backwards, the first child may not be the closest to the scrollOffset, so we need to look backwards to find the closest one.

    // Find the first child that ends after the scroll offset.
    while (endScrollOffset < scrollOffset) {
      // If the value is less than or equal to a value, the value needs to be recycled.
      leadingGarbage += 1;
      if(! advance()) {assert(leadingGarbage == childCount);
        assert(child == null);
        // If none is satisfied, the last child will prevail
        // we want to make sure we keep the last child around so we know the end scroll offset
        collectGarbage(leadingGarbage - 1.0);
        assert(firstChild == lastChild);
        final double extent = childScrollOffset(lastChild) + paintExtentOf(lastChild);
        geometry = SliverGeometry(
          scrollExtent: extent,
          paintExtent: 0.0,
          maxPaintExtent: extent,
        );
        return; }}Copy the code
  • Process the child backwards until the end of the layout area.
    // Now find the first child that ends after our end.
    // Until the end of the layout area
    while (endScrollOffset < targetEndScrollOffset) {
      if(! advance()) { reachedEnd =true;
        break; }}// Finally count up all the remaining children and label them as garbage.
    // This is the last child to be laid out, so any child after it will be recycled
    if(child ! =null) {
      child = childAfter(child);
      while(child ! =null) {
        trailingGarbage += 1; child = childAfter(child); }}Copy the code
  • Recovery of the children
    // At this point everything should be good to go, we just have to clean up
    // the garbage and report the geometry.
    // Use the previously calculated reclaim parameters
    collectGarbage(leadingGarbage, trailingGarbage);
 
  @protected
  void collectGarbage(int leadingGarbage, int trailingGarbage) {
    assert(_debugAssertChildListLocked());
    assert(childCount >= leadingGarbage + trailingGarbage);
    invokeLayoutCallback<SliverConstraints>((SliverConstraints constraints) {
      // Delete from the first one backwards
      while (leadingGarbage > 0) {
        _destroyOrCacheChild(firstChild);
        leadingGarbage -= 1;
      }
      // Delete from the last one
      while (trailingGarbage > 0) {
        _destroyOrCacheChild(lastChild);
        trailingGarbage -= 1;
      }
      // Ask the child manager to remove the children that are no longer being
      // kept alive. (This should cause _keepAliveBucket to change, so we have
      // to prepare our list ahead of time.)
      _keepAliveBucket.values.where((RenderBox child) {
        final SliverMultiBoxAdaptorParentData childParentData = child.parentData as SliverMultiBoxAdaptorParentData;
        return! childParentData.keepAlive; }).toList().forEach(_childManager.removeChild);assert(_keepAliveBucket.values.where((RenderBox child) {
        final SliverMultiBoxAdaptorParentData childParentData = child.parentData as SliverMultiBoxAdaptorParentData;
        return! childParentData.keepAlive; }).isEmpty); }); }void _destroyOrCacheChild(RenderBox child) {
    final SliverMultiBoxAdaptorParentData childParentData = child.parentData as SliverMultiBoxAdaptorParentData;
    // If the child is marked for cache, remove it from the tree and place it in the cache
    if (childParentData.keepAlive) {
      assert(! childParentData._keptAlive); remove(child); _keepAliveBucket[childParentData.index] = child; child.parentData = childParentData;super.adoptChild(child);
      childParentData._keptAlive = true;
    } else {
      assert(child.parent == this);
      // Remove it directly
      _childManager.removeChild(child);
      assert(child.parent == null); }}Copy the code
  • Compute the output of Silver
    assert(debugAssertChildListIsNonEmptyAndContiguous());
    double estimatedMaxScrollOffset;
    // And at the end, use the end of the last child directly
    if (reachedEnd) {
      estimatedMaxScrollOffset = endScrollOffset;
    } else {
    // Calculate the estimated maximum value
      estimatedMaxScrollOffset = childManager.estimateMaxScrollOffset(
        constraints,
        firstIndex: indexOf(firstChild),
        lastIndex: indexOf(lastChild),
        leadingScrollOffset: childScrollOffset(firstChild),
        trailingScrollOffset: endScrollOffset,
      );
      assert(estimatedMaxScrollOffset >= endScrollOffset - childScrollOffset(firstChild));
    }
    // Calculate the size of remainingPaintExtent
    final double paintExtent = calculatePaintOffset(
      constraints,
      from: childScrollOffset(firstChild),
      to: endScrollOffset,
    );
    // Calculate the current size of the cache drawing region consumed by remainingCacheExtent
    final double cacheExtent = calculateCacheOffset(
      constraints,
      from: childScrollOffset(firstChild),
      to: endScrollOffset,
    );
    // Layout the end of the area
    final double targetEndScrollOffsetForPaint = constraints.scrollOffset + constraints.remainingPaintExtent;
    // The output is fed back to the Viewport. The Viewport uses the output from the sliver to layout the next one if the sliver is empty
    geometry = SliverGeometry(
      scrollExtent: estimatedMaxScrollOffset,
      paintExtent: paintExtent,
      cacheExtent: cacheExtent,
      maxPaintExtent: estimatedMaxScrollOffset,
      // Conservative to avoid flickering away the clip during scroll.
      // Whether clip is required
      hasVisualOverflow: endScrollOffset > targetEndScrollOffsetForPaint || constraints.scrollOffset > 0.0,);// We may have started the layout while scrolled to the end, which would not
    // expose a new child.
    // If the two are equal, the bottom of the sliver is already there
    if (estimatedMaxScrollOffset == endScrollOffset)
      childManager.setDidUnderflow(true);
    // The notification completes the layout
    / / here will pass [SliverChildDelegate didFinishLayout] will be the first last conveyed index, index and can be used to track
    childManager.didFinishLayout();

Copy the code

Estimate the maximum value by default

  static double _extrapolateMaxScrollOffset(
    int firstIndex,
    int lastIndex,
    double leadingScrollOffset,
    double trailingScrollOffset,
    int childCount,
  ) {
    if (lastIndex == childCount - 1)
      return trailingScrollOffset;
    final int reifiedCount = lastIndex - firstIndex + 1;
    // Calculate the average value
    final double averageExtent = (trailingScrollOffset - leadingScrollOffset) / reifiedCount;
    // Add the remaining estimate
    final int remainingCount = childCount - lastIndex - 1;
    return trailingScrollOffset + averageExtent * remainingCount;
  }
Copy the code

Silver paint

RenderSliverMultiBoxAdaptor

  • Paint method
  @override
  void paint(PaintingContext context, Offset offset) {
    if (firstChild == null)
      return;
    // offset is to the top-left corner, regardless of our axis direction.
    // originOffset gives us the delta from the real origin to the origin in the axis direction.
    Offset mainAxisUnit, crossAxisUnit, originOffset;
    bool addExtent;
    // Get the coefficient of the main axis and the horizontal axis according to the direction of the roll
    switch (applyGrowthDirectionToAxisDirection(constraints.axisDirection, constraints.growthDirection)) {
      case AxisDirection.up:
        mainAxisUnit = const Offset(0.0.1.0);
        crossAxisUnit = const Offset(1.0.0.0);
        originOffset = offset + Offset(0.0, geometry.paintExtent);
        addExtent = true;
        break;
      case AxisDirection.right:
        mainAxisUnit = const Offset(1.0.0.0);
        crossAxisUnit = const Offset(0.0.1.0);
        originOffset = offset;
        addExtent = false;
        break;
      case AxisDirection.down:
        mainAxisUnit = const Offset(0.0.1.0);
        crossAxisUnit = const Offset(1.0.0.0);
        originOffset = offset;
        addExtent = false;
        break;
      case AxisDirection.left:
        mainAxisUnit = const Offset(1.0.0.0);
        crossAxisUnit = const Offset(0.0.1.0);
        originOffset = offset + Offset(geometry.paintExtent, 0.0);
        addExtent = true;
        break;
    }
    assert(mainAxisUnit ! =null);
    assert(addExtent ! =null);
    RenderBox child = firstChild;
    while(child ! =null) {
      // Get the position of the child spindle, subtracting the scrollOffset for the child's layoutOffset
      final double mainAxisDelta = childMainAxisPosition(child);
      // Get the position of the horizontal axis of the child, with the ListView being 0.0 and the GridView being the calculated crossAxisOffset
      final double crossAxisDelta = childCrossAxisPosition(child);
      Offset childOffset = Offset(
        originOffset.dx + mainAxisUnit.dx * mainAxisDelta + crossAxisUnit.dx * crossAxisDelta,
        originOffset.dy + mainAxisUnit.dy * mainAxisDelta + crossAxisUnit.dy * crossAxisDelta,
      );
      if (addExtent)
        childOffset += mainAxisUnit * paintExtentOf(child);

     
      // If the child's visible interval (mainAxisDelta, mainAxisDelta + paintExtentOf(child))
      // does not intersect the paint extent interval (0, constraints.remainingPaintExtent), it's hidden.
      // You can see that there are some children that don't need to be drawn in the visible area because of the cache
      if (mainAxisDelta < constraints.remainingPaintExtent && mainAxisDelta + paintExtentOf(child) > 0) context.paintChild(child, childOffset); child = childAfter(child); }}Copy the code

RenderSliverFixedExtentList

When the ListView itemExtent is not null, use RenderSliverFixedExtentList. And we’re just going to talk about this briefly, because knowing the height of the principal axis of the child makes it easier to do all kinds of calculations. We can compute the first child and the last child directly from the scrollOffset and viewport.

GridView

RenderSliverGrid

Finally, our GridView, because the GridView is designed so that the main axis size of the child is equal to the size of the horizontal axis/the number of children on the horizontal axis (also depending on the childAspectRatio ratio (default is 1.0)), So the size of the main axis of the child is also known, and the position of the horizontal axis is easy to determine. The basic calculation is pretty much the same as the ListView.

The lines

Talked about a bunch of source code, do not know how many people can see here. Through source code analysis, we learned some computational plotting knowledge of the Silver list. Next we’ll extend the official Sliver list to accommodate the shy effect.

Image list memory optimization

It is common to hear people say that the image list will blink after scrolling a few times. This is particularly noticeable on ios, but on Android the memory grows rapidly because the Flutter buffers images in memory by default. If you load 300 images in the scroll list, you will have a memory cache of 300 images in memory. The official cache limit is 1000.

List memory test

First, let’s look at the memory of the list of images without any processing. I made a list of images here, the common 9 grid image list, the total number of incrementally loaded children is 300, which means that after loading, there may be (1 9)*300=(300 2700) images in memory cache, of course because the official cache is 1000, The final image memory cache should be between 300 and 1000 (if the total image size does not exceed the official limit).

Memory detection tool

  • First of all, executionflutter packages pub global activate devtoolsActivate the dart devtools

  • After the activation is successful, execute

flutter --no-color packages pub global run devtools --machine --port=0 Enter the 127.0.0.1:9540 address shown above into your browser.

  • Next we need to executeflutter run --profileLet’s run our test application

When we’re done, we’ll have an address that we’ll copy into DevTools’ Connect

  • After clicking Connect, switch to Memory at the top, and we can see the real-time Memory change monitoring of the application

Tests that do not do any processing

  • Android, I open up the list, and I keep pulling down until the 300 bars are loaded, and the memory changes as shown below, you can see the memory takes off and explodes

  • For ios, I did the same thing, but unfortunately, it didn’t make it to the end, and it popped out around 600m (related to ios app memory limit).

The example above clearly shows the large memory consumption of multiple image lists. We looked at the whole list drawing process in The Flutter earlier, so is there any way we can improve memory? The answer is that we can try to clear the memory cache that contains the image in that child when the list children is recycled. In this way, there is only a small amount of memory for images in our list. On the other hand, since our images are cached in the hard disk, even if we clear the memory cache, the images will not be downloaded again when they are reloaded, which is not aware to the user.

Image memory optimization

With the latest update, you can remove more image memory by setting it directly

     ExtendedImage(
      clearMemoryCacheWhenDispose: true.)Copy the code

We mentioned earlier the official collectGarbage method, which is called to remove unwanted children. Then we can get the indexes of the cleared children at this point and notify the user.

The key code is as follows. Since I don’t want to override any more of Silver’s underlying classes, I’m passing the indexes through a callback in the ExtendedListDelegate.

  void callCollectGarbage({
    CollectGarbage collectGarbage,
    int leadingGarbage,
    int trailingGarbage,
    int firstIndex,
    int targetLastIndex,
  }) {
    if (collectGarbage == null) return;

    List<int> garbages = []; firstIndex ?? = indexOf(firstChild); targetLastIndex ?? = indexOf(lastChild);for (var i = leadingGarbage; i > 0; i--) {
      garbages.add(firstIndex - i);
    }
    for (var i = 0; i < trailingGarbage; i++) {
      garbages.add(targetLastIndex + i);
    }
    if(garbages.length ! =0) {
      //call collectGarbagecollectGarbage.call(garbages); }}Copy the code

When notified that chilren is cleared, the image cache is removed from memory using the imageProvider.evict method.

    SliverListConfig<TuChongItem>(
      collectGarbage: (List<int> indexes) {
        ///collectGarbage
        indexes.forEach((index) {
           final item = listSourceRepository[index];
            if (item.hasImage) {
            item.images.forEach((image) {
              finalprovider = ExtendedNetworkImageProvider( image.imageUrl, ); provider.evict(); }); }}); },Copy the code

After optimization perform the same steps, Android memory change to below

The same is true for ios

Not limiting enough?

From the above test, we can see that after optimization, the memory of the picture list has been greatly optimized, basically meeting our needs. But we’re not going too far, because for list images, we usually don’t have that high quality.

  • Use the official ResizeImage, which was recently added to reduce the image memory cache. You can reduce the image by setting width/height. Use the following

This, of course, assumes that you know the size of the image in advance so that you can compress the image proportionally. For example, in the following code I reduced the width and height by 5 times. Note that when you do this, the quality of the image will deteriorate and if it is too small, it will burn out. Please set it according to your own situation. Another problem is that the list of images and click on the image preview image, because it is not the same ImageProvider (preview images generally want to be hd), so it will be downloaded repeatedly, please choose according to your own situation.

The code address

  ImageProvider createResizeImage() {
    return ResizeImage(ExtendedNetworkImageProvider(imageUrl),
        width: width ~/ 5, height: height ~/ 5);
  }
Copy the code
  • In succession ExtendedNetworkImageProvider (of course other provider also extended through this method to compress image), override instantiateImageCodec method, here to compress images.

Code position

  ///override this method, so that you can handle raw image data,
  ///for example, compress
  Future<ui.Codec> instantiateImageCodec(
      Uint8List data, DecoderCallback decode) async {
    _rawImageData = data;
    return await decode(data);
  }
Copy the code
  • After these optimizations were made, we ran the test again to check the memory changes, and the memory consumption was reduced again.

Support my PR

If the solution works for you, please support my PR of collectGarbage.

add collectGarbage method for SliverChildDelegate to track which children can be garbage collected

This will allow more people to solve the image list memory problem. You can also use ExtendedList WaterfallFlow and LoadingMoreList directly, both of which support this API. I have submitted the complete solution to ExtendedImage demo for the convenience of viewing the whole process.

List exposure tracking

Simply put, how can we easily know the children in the visual area? In fact, we can easily obtain the children’s indexes in the visual area during the calculation and drawing of the list. I’ve provided the ViewportBuilder callback to get the first index and the last index in the visible region. Code position

Again through the ExtendedListDelegate, callbacks are made in the viewportBuilder.

Using the demonstration

        ExtendedListView.builder(
            extendedListDelegate: ExtendedListDelegate(
                viewportBuilder: (int firstIndex, int lastIndex) {
                print("viewport : [$firstIndex.$lastIndex]. "");
                }),
Copy the code

Specialization the layout of the last child

The example we saw when we started our Flutter, doing incremental load lists, was to use the last child as loadmore/no more. The ListView doesn’t have any problems when it’s full screen, but here’s what needs to be fixed.

  • When the ListView is full, the last child shows’ no more ‘. Usually you want ‘no more’ to be displayed at the bottom, but since it’s the last child, it will be next to the last.
  • When the last GridView child is used as loadMore/No more. The product does not want them to be laid out as normal GridView elements

In order to solve this problem, I design the lastChildLayoutTypeBuilder. Layout the last child using the type of the last child told by the user. Let’s take RenderSliverList as an example.

    if (reachedEnd) {
      ///zmt
      finallayoutType = extendedListDelegate? .lastChildLayoutTypeBuilder ? .call(indexOf(lastChild)) ?? LastChildLayoutType.none;// Size of the last child
      final size = paintExtentOf(lastChild);
      // The end position of the last child
      final trailingLayoutOffset = childScrollOffset(lastChild) + size;
      / / if the end of the last child drawing location map size is smaller than the rest, then we will be the location of the last child to constraints. RemainingPaintExtent - size
      if (layoutType == LastChildLayoutType.foot &&
          trailingLayoutOffset < constraints.remainingPaintExtent) {
        final SliverMultiBoxAdaptorParentData childParentData =
            lastChild.parentData;
        childParentData.layoutOffset = constraints.remainingPaintExtent - size;
        endScrollOffset = constraints.remainingPaintExtent;
      }
      estimatedMaxScrollOffset = endScrollOffset;
    }
Copy the code

Finally, let’s see how to use it.

        enum LastChildLayoutType {
        /// ordinary
        none,

        /// Draw the last element after the maximum spindle Item and use the horizontal axis size as the layout size
        /// This is used in [ExtendedGridView] and [WaterfallFlow] when the last element is a loadmore/no more element.
        fullCrossAxisExtend,

        /// Draw the last child on the Trailing of Viewport and use the horizontal axis size as the layout size
        /// This is usually used when the last element is a loadmore/no more element and the list elements do not fill the entire viewport
        /// If the list elements are full of viewport, the effect is the same as fullCrossAxisExtend
        foot,
        }

      ExtendedListView.builder(
        extendedListDelegate: ExtendedListDelegate(
            // The total length of the list should be length + 1
            lastChildLayoutTypeBuilder: (index) => index == length
                ? LastChildLayoutType.foot
                : LastChildLayoutType.none,
            ),
Copy the code

Simple chat list

When we make a chat list, because the layout is from the top down, our first reaction is to set the ListView reverse to true. When a new session is inserted at zero, this is the easiest setting, but when the session is not full of viewports, because the layout is flipped, So the layout will look something like this.

     trailing
-----------------
|               |
|               |
|     item2     |
|     item1     |
|     item0     |
-----------------
     leading
Copy the code

To solve this problem, you can set closeToTrailing to true and the layout will become as follows. This property also supports [ExtendedGridView],[ExtendedList], and [WaterfallFlow]. Of course, if reverse is not true, you can still set this property, and the layout will follow the trailing when the viewport is not full.

     trailing
-----------------
|     item2     |
|     item1     |
|     item0     |
|               |
|               |
-----------------
     leading
Copy the code

How is that realistic? For this I added two extension methods

  • handleCloseToTrailingEnd

If the end of the last child is not as large as the rest of the draw area, Then we add constraints to every child’s drawing start. RemainingPaintExtent – endScrollOffset distance, the phenomenon is all the children are close to the trailing layout. This method is called after the overall layout is calculated.

  /// handle closeToTrailing at end
  double handleCloseToTrailingEnd(
      bool closeToTrailing, double endScrollOffset) {
    if (closeToTrailing && endScrollOffset < constraints.remainingPaintExtent) {
      RenderBox child = firstChild;
      final distance = constraints.remainingPaintExtent - endScrollOffset;
      while(child ! =null) {
        final SliverMultiBoxAdaptorParentData childParentData =
            child.parentData;
        childParentData.layoutOffset += distance;
        child = childAfter(child);
      }
      return constraints.remainingPaintExtent;
    }
    return endScrollOffset;
  }
Copy the code
  • handleCloseToTrailingBegin

Because we give each child’s drawing beginning added constraints remainingPaintExtent – endScrollOffset distance. The next time we performLayout, we should remove this distance first. When the index of the first child is 0 and the layoutOffset is not 0, we need to remove all children’s layoutoffsets.

  /// handle closeToTrailing at begin
  void handleCloseToTrailingBegin(bool closeToTrailing) {
    if (closeToTrailing) {
      RenderBox child = firstChild;
      SliverMultiBoxAdaptorParentData childParentData = child.parentData;
      // Remove all the distance increased by the previous performLayout
      if (childParentData.index == 0&& childParentData.layoutOffset ! =0) {
        var distance = childParentData.layoutOffset;
        while(child ! =null) { childParentData = child.parentData; childParentData.layoutOffset -= distance; child = childAfter(child); }}}}Copy the code

Finally, let’s see how to use it.

      ExtendedListView.builder(
        reverse: true,
        extendedListDelegate: ExtendedListDelegate(closeToTrailing: true),
Copy the code

conclusion

In this chapter, we analyze the source code of the Sliver list, and solve some problems in the actual development. In the next chapter we will create our own waterfall flow layout. You will also have the ability to create any sliver layout list.

Welcome to joinFlutter CandiesTogether they produce cute little candies called FlutterQQ group: 181398081

Finally put the Flutter on the bucket. It smells good.