Overview: Listview solution for resolving the complex flutter layout process and extending the underlying capabilities

background

Currently, both the home page and the search page of Idle Fish business have a large number of scenes that can land waterfall flow, while Flutter native only provides ListView and GridView, which cannot provide the ability to customize the layout.

In the community, the general waterfall flow solution is based on the SliverMultiBoxAdaptor to customize its performLayout, the main problem is the lack of reuse mechanism, and in many cases prone to repeated layout. In complex scenarios of online services, the number of frames is low and the screen flashes may occur. At the same time, support for Child life cycle, dot exposure and other basic functions is still blank.

Therefore, we urgently need a more general list-view solution that can solve the complex layout process and extend the basic capabilities.

An introduction to list views in Flutter

! [](https://pic1.zhimg.com/80/v2-31f249b7389b110171d0eac51957c9bb_720w.jpg)

Scrollable is a StatefulWidget that listens for gesture input from the user. The State build method returns a ViewportScrollPosition with a Listener and RawGestureDetector to describe its location and defines onStart, onUpdate, onEnd and other callbacks within it. The start and end of each slide in the Scrollable corresponds to a Darg object and sends a notification of the slide. The Viewport listens for notifications.

2. The Sliver Flutter has two layouts. During layout, each Sliver receives a SliverConstraints evaluation that returns a SliverGeometry, in the same way that RenderBox receives a Size from BoxConstraints. Sliver is managed by Viewport.

3. Viewport A widget that is bigger on the inside. Viewport holds one or more slivers. The Scrollable passes the offset to the Viewport, which decides which slivers should be Visible. Viewport is essentially a MultiChildRenderObjectWidget, that is, the scroll view main rendering logic is completed in the Viewport.

In performLayout, _attemptLayout is centered on Center, and layouts the leading child followed by the trailing child. Only dirty children are laid out.

do { correction = _attemptLayout(mainAxisExtent, crossAxisExtent, offset.pixels + centerOffsetAdjustment); if (correction ! = 0.0) {offset.. correctBy (correction); } else {the if (offset.. applyContentDimensions (math.h min (0.0, _minScrollExtent + mainAxisExtent * anchor), math. Max (0.0, _maxScrollExtent - mainAxisExtent * (1.0-anchor)),)) break; } count += 1; } while (count < _maxLayoutCycles); If _attemptLayout returns a non-zero correction, the process of the current layout is broken, and the layout needs to be restarted after adjusting the offset, with a maximum of 10 consecutive breaks (_maxLayoutCycles). As long as the correction is correct, for example, the targetScrollOffset is very far, and the child is exhausted during scroll, the Sliver will need to make the correction to the Viewport. However, Flutter is not a sliding effect achieved by constantly changing the position of the child by layout. Such redrawing process is obviously inefficient. Obviously, the RenderObject does not need to be changed and can be reused. But layout generally only happens during the process of adding a new child, and sliding happens during paint. Void _paintWithContext(PaintingContext context, Offset Offset) {// If (_needsLayout) return; _needsPaint = false; paint(context, offset); }Copy the code

The Viewport indirectly holds the Canvas for drawing through the PaintingContext. Offset refers to the cartesian coordinate system, independent of the Axis direction. Just change the Offset of the corresponding RenderObject when drawing to achieve the scrolling effect, so that the RenderObject does not need to be recreated. So if we want to achieve a high performance list view, we will try to reduce the relayout of the Child. With a basic understanding of the list layout of Flutter, let’s take a look at the implementation of waterfall flow.

Waterfall flow implementation logic

The WatetfallFlow layout needs to specify the Child’s Offset and then lay it out. So you need to inherit from SliverMultiBoxAtaptor, depending on its ability to convert SliverConstraints to BoxConstraints. We can also use its SliverBoxChildManager to control lazy loading of the Child.

The core logic

In waterfall flow, because the child (mostly) in the same row (column) has a sequential relationship and needs to be laid out in order, waterfall flow is more similar to ListView than GridView, and the layout process of waterfall flow is also borrowed from ListView. The logic of the waterfall flow layout revolves around three core elements:

  1. Find the child closest to the edge of the slide, add the child after (before), and layout the child.
  2. GC is performed after the child has left a certain distance.
  3. Ensure that the Layout method is called as little as possible. Layout calls performLayout instead of paint.

The core data structure is ParentData.

ParentData resides in the Child, which passes it to the Sliver, which passes it to the upper layer, which stores all the layout information (in Cartesian coordinates). In performLayout, the layout information used by the Child when calling layout comes from ParentData. In the process of adding a Child, a Manager is used to store the ParentData of all children on the front and back edges. When adding a Child, find the Child whose edge is closest to the visible area, set its ParentData and replace the current Child.

The core logic of the layout is to lay out from the beginning Child (for firstIndex) to the end Child (for targetLastIndex). If there are already records in _layoutedChilds, its layout is skipped.

for (int index = firstIndex; index <= targetLastIndex; ++index) { final SliverGeometry gridGeometry = layout.getGeometryForChildIndex(index); final BoxConstraints childConstraints = gridGeometry.getBoxConstraints(constraints); RenderBox child = childAfter(trailingChildWithLayout); if (child == null || indexOf(child) ! = index) {/ / retrieves the Child. The Child = _createAndLayoutChildIfNeeded (childConstraints, after: trailingChildWithLayout); if (child ! = null && indexOf(child) == index) { _layoutedChilds.add(index); }else if (child == null) {// Child is exhausted. } } else { if (! _layoutedChilds.contains(index)) { _layoutChildIfNeeded(child, parentUsesSize: true); _layoutedChilds.add(index); } } trailingChildWithLayout = child; }Copy the code

GC the child leaving the view, and remember to clear the child from the array.

if (firstChild ! Final int oldFirstIndex = indexOf(firstChild); final int oldLastIndex = indexOf(lastChild); Final int leadingGarbage = (firstindex-oldFirstindex). Clamp (0, childCount); final int trailingGarbage = targetLastIndex == null ? 0 : (oldLastIndex - targetLastIndex).clamp(0, childCount); // GC collectGarbage(leadingGarbage, trailingGarbage); _layoutedChilds.sort(); _layoutedChilds.removeRange(0, leadingGarbage); _layoutedChilds.removeRange(layoutedChilds.length - 1 - trailingGarbage, layoutedChilds.length - 1); } else { collectGarbage(0, 0); }Copy the code

In the process of development, the problem of low frame number occurred, and it was found that the duplicate layout of Child appeared in the process of performLayout. The solution is to record not only the leading, trailing edges of the child. It also provides the ability to update a single child’s layout individually. When updating a Child’s layout, you simply remove the corresponding Child from the record.

In contrast to the native view, we can provide real-time and efficient callbacks to the upper-layer interface by getting the ParentData information of all children. This provides life-cycle, exposure dot capability based on the real-time location of each Child. Therefore, the coordinates of each child can be monitored to obtain accurate exposure information.

From the waterfall to the container

Some design problems were also exposed during the development of the waterfall stream.

For example, the specific rendering logic of waterfall flow is carried out in RenderObject, which is too low-level for the business side to customize according to the business.

As there is no mechanism for reuse, frames will inevitably decrease due to repeated rendering when the view hierarchy is more complex.

! [](https://pic3.zhimg.com/80/v2-2a860101e337d0e418b7b3f05e1e562f_720w.jpg)

The whole container is divided into three parts after the redesign based on native ideas.

1, the delegate

We manage the child lifecycle and respond to gestures, and since we can get the parentData property of each visible child, we can be notified in real time as we scroll. Thus listening for the position of each Child from the beginning of the creation into the buffer to the visible region from the buffer. Gestures come from the top layer of Scrollable.

2, layout,

Responsible for the layout of all children. Pull out the layout logic, similar to UICollectionViewLayout in iOS. However, there were some problems in the development process, mainly due to the special information transmission mode of Flutter. That is, we could not calculate the layout of all the children at one time in the native way. Because the RenderBox needs to receive a BoxConstraints to return a size.

3, reuser

Reuser, at the RenderObject level, reuses children based on type and performs local updates. Need to copy a copy of SliverMultiBoxAdaptor and its Element to rewrite, change its mount logic, solution is still exploring and research, hope to meet you in the next article!

The performance data

! [](https://pic3.zhimg.com/80/v2-06195c075253df672331075508710afb_720w.png)

Applied to the main search page for automated testing, it was around 54.7 frames before, and 56.2 after switching to waterfall stream, which is about 1.5 frames up.

! [](https://pic4.zhimg.com/80/v2-022855172537a8824fcd03b92a86dafe_720w.jpg)

There is a slight increase in memory.

Looking forward to

There are still many problems to be solved in the list view of Flutter, such as scrollTo(int index) capability in waterfall stream, memory usage, etc. There are still some problems in the stability and compatibility of the reuse of Flutter side. Idle fish still have a long way to go on Flutter. PS0: The code in this article is based on Flutter 1.12.13. PS1: The Viewport, for example, refers to both the Widget itself and its corresponding RenderObject. PS2: The code involved in this article has been deleted and modified for reference only.

Author: Idle fish technology – Night Waves

The original link

This article is the original content of Aliyun and shall not be reproduced without permission.