preface

This article is a small summary of recent listView research on Flutter;

Actually, I’ve never been a fan of the Design of The Flutter, especially since I was working on the novel reader that supports simulated page-turning. More than once I thought, “What’s this md doing? Look at Android XXX next door.

PS: Actually, I still think the same thing about text drawing;

But I recently saw idle Fish’s Flutter high-performance, multifunctional, full-scene rolling container, so I must check it out! I came up with some ideas to improve and rewrite the novel reader. After studying the listView design of Flutter, I found that the design of Flutter seems to be quite good. The idea of Flutter is very clear and lightweight, while Android is quite heavy by comparison. (Maybe this is because the current listView is too early, but the design idea is pretty easy to read)

PPS: Is the custom engine version of Idle Fish too low to have those features? Things like exposure and reuse are already in place… Why bother customizing?

The actors are in position and the show begins

First, assume that you already know the basics of Flutter, such as the three trees and what they do. Of course, did not understand the relationship is not too big, Baidu Google, the explanation of this thing has rotten streets, see a 5 minutes to understand about enough;

PS: If too lazy to see, directly turn over the back of the summary part, step to the stomach;

Widget trees and Element trees are relatively lightweight because they do not participate in the drawing process, so it seems to me that reusing renderObject can improve performance a lot, so here’s the problem:

How to reuse renderObject?

To solve this problem, start tracking RenderObject’s love-hate relationship with Element;

Of course, I don’t want to mention the details of their relationship here, but go straight to the relevant part;

The widget tree? That thing is a licking dog, easy to come and go, don’t care;

As we all know, to display a View, there must be three steps: measure, layout and draw; The same applies to Flutter, except that renderObject does this; To find the relevant section, find the relevant method in the listView:

class RenderSliverList extends RenderSliverMultiBoxAdaptor { /// Creates a sliver that places multiple box children in a  linear array along /// the main axis. /// /// The [childManager] argument must not be null. RenderSliverList({ required  RenderSliverBoxChildManager childManager, }) : super(childManager: childManager); @override void performLayout() {………… 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!) ; 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. 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; childParentData.layoutOffset = endScrollOffset; assert(childParentData.index == index); endScrollOffset = childScrollOffset(child!) ! + paintExtentOf(child!) ; return true; } // Find the first child that ends after the scroll offset. while (endScrollOffset < scrollOffset) { leadingGarbage += 1; if (! advance()) { assert(leadingGarbage == childCount); assert(child == null); // 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; } } // Now find the first child that ends after our end. while (endScrollOffset < targetEndScrollOffset) { if (! advance()) { reachedEnd = true; break; }}... collectGarbage(leadingGarbage, trailingGarbage); ... }}Copy the code

Notice advance(), which has the insertAndLayoutChild method, which is literally responsible for inserting the child RenderObject

@protected RenderBox? insertAndLayoutChild( BoxConstraints childConstraints, { required RenderBox? after, bool parentUsesSize = false, }) { assert(_debugAssertChildListLocked()); assert(after ! = null); final int index = indexOf(after!) + 1; _createOrObtainChild(index, after: after); final RenderBox? child = childAfter(after); if (child ! = null && indexOf(child) == index) { child.layout(childConstraints, parentUsesSize: parentUsesSize); return child; } childManager.setDidUnderflow(true); return null; }Copy the code

Alas, there is a _createOrObtainChild method, which literally translates to create or retrieve a child? Access to the child?

Click on it again

  void _createOrObtainChild(int index, { required RenderBox? after }) {
    invokeLayoutCallback<SliverConstraints>((SliverConstraints constraints) {
      assert(constraints == this.constraints);
      if (_keepAliveBucket.containsKey(index)) {
        final RenderBox child = _keepAliveBucket.remove(index)!;
        final SliverMultiBoxAdaptorParentData childParentData = child.parentData as SliverMultiBoxAdaptorParentData;
        assert(childParentData._keptAlive);
        dropChild(child);
        child.parentData = childParentData;
        insert(child, after: after);
        childParentData._keptAlive = false;
      } else {
        _childManager.createChild(index, after: after);
      }
    });
  }
Copy the code

The createChild function is not cache-specific, so the _keepAliveBucket function is not cache-specific. But who added the objects that were being reused?

Click on it and see who’s using _keepAliveBucket

Em, seems to be used by many people, but not kansai; Anyway, I just want to know who put the data in there, so once again, there are only two ways:

One is the move method, which doesn’t look like the name; One is the _destroyOrCacheChild method, which looks like it. Click on it.

void _destroyOrCacheChild(RenderBox child) { final SliverMultiBoxAdaptorParentData childParentData = child.parentData as  SliverMultiBoxAdaptorParentData; 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); _childManager.removeChild(child); assert(child.parent == null); }}Copy the code

So there is a new thing: SliverMultiBoxAdaptorParentData, so just let this thing keepAlive is true

Let’s see who’s calling this _destroyOrCacheChild;

Well, there’s only one method, collectGarbage, that’s only called in performLayout, and it’s either directly or indirectly called in performLayout, just like the _createOrObtainChild method. Belongs to the more direct life cycle class method;

Simply override the collectGarbage method to say that keepAlive is true for objects to be collected.

Here it is ?????

The createChild method is called only once for each index; the createChild method is called only once for each index. Should be successful reuse;

Expansion and conjecture

So that solves the reuse problem? These two words are really what I thought at that time. It even feels like it’s not so simple. Is there a pit in it?

But this leads me to a few guesses:

If so, can you reuse RenderObjects as thoroughly as RecyclerView?

Now we’re going to reuse it based on index, very simple; If you swipe to a new item and it doesn’t exist in the cache, you still go create;

But what if, like Android RecyclerView, you could just take the RenderObject from the cache and put it back in instead of creating it, and update the display data through an update mechanism? The internal update of flutter uses a diff algorithm. This should be more efficient than simply creating flutter

conclusion

We can reuse this piece directly using the official information provided by Flutter. Of course, this is purely experimental. There are no defects in flutter, nor have we done a comprehensive test

This is my information about the flutter part of the flutter doctor-v. If your code is different, you can refer to it for comparison:

[√] Flutter (Channel Stable, 1.22.5, on Microsoft Windows [Version 10.0.17763.1577], Locale zh-CN) • Flutter version 1.22.5 at D: Program File\ SDK \flutter_windows_v1.9.1+hotfix • Engine Revision AE90085A84 • Dart Version 2.10.4Copy the code

In the key part of the code below, the default custom RenderSliverList has been introduced through inheritance and overwriting

class RecyclerRenderSliverList extends RenderSliverList { RecyclerRenderSliverList({ @required RenderSliverBoxChildManager childManager, }) : super(childManager: childManager); @override void collectGarbage(int leadingGarbage, If (leadingGarbage + trailingGarbage!) {// if (leadingGarbage + trailingGarbage! = 0) { print("collectGarbage : " + " leadingGarbage : " + leadingGarbage.toString() + ", trailingGarbage : " + trailingGarbage.toString()); if (childCount >= leadingGarbage + trailingGarbage) { int tempLeadingGarbage = leadingGarbage; int tempTrailingGarbage = trailingGarbage; RenderObject tempFirstChild = firstChild; RenderObject tempLastChild = lastChild; While (tempLeadingGarbage > 0) {/ / / tag keepAlive to true (tempFirstChild. ParentData as SliverMultiBoxAdaptorParentData) .keepAlive = true; tempFirstChild = childAfter(tempFirstChild); tempLeadingGarbage -= 1; } while (tempTrailingGarbage > 0) {/ / / tag keepAlive to true (tempLastChild. ParentData as SliverMultiBoxAdaptorParentData) .keepAlive = true; tempLastChild = childBefore(tempLastChild); tempTrailingGarbage -= 1; }}} super.collectgarbage (leadingGarbage, trailingGarbage); }}Copy the code

It’s that simple …………

If you want to have 10,000 items, it will cache 10,000 items for you, so in theory it will eat up memory. The correct way to do this should be to combine their own cache rules, but that block has not been done

So further tests are needed

And a little bit more:

Exposure this is pretty simple, does this help renderObject parentData SliverMultiBoxAdaptorParentData, take all the index inside…

The value of offset saved by traversing the child is the first visible item. Then just grab index from parentData and you’re done

I really don’t have a problem with that

digression

If you still need to, you can pull down the Dev branch of Flutter_novel. The reader2 folder in the dev branch is the related part, but you need to find it yourself.

If nothing goes wrong, I will be squatting in Tiwat for the next 12 days. So, it is normal to lose contact with each other, unless we meet in Tiwat and say:

So you play the original god?

Well, that’s awkward