【Flutter技术】ListView即将迎来重要更新,这些场景的性能将大大提升

0 ListView的性能瓶颈

我们知道,ListView等长列表在滚动的过程中是Lazy Loading机制,按需加载滑窗范围内的items,但如果items的高度是没有显性的指定的时候,将会有严重的性能问题,该性能问题的根因我在《社区说|Flutter 长列表 Lazy Loading 机制解析》做过详细的分析,感兴趣的同学可以了解一下,有助于理解Flutter的长列表加载机制。

这个性能问题也困扰了社区多年,ListView: Poor performance with many variable-extent items + jumpTo (scroll bar, trackpad, mouse wheels),受到了很多开发者的关注:

这里我写了一个Demo,大家可以对比下有ListView.itemExtent和没有设置ListView.itemExtent的性能差异:

dart 复制代码
import 'dart:ui';

import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';

void main() {
  runApp(
    MaterialApp(
      // showPerformanceOverlay: true,
      scrollBehavior: MyScrollBehavior(),
      home: const Scaffold(
        body: ExampleApp(),
      ),
    ),
  );
}

class ExampleApp extends StatefulWidget {
  const ExampleApp({Key? key}) : super(key: key);

  @override
  State<StatefulWidget> createState() {
    return ExampleAppState();
  }
}

class ExampleAppState extends State<ExampleApp> {
  final scrollController = ScrollController();

  @override
  Widget build(BuildContext context) {
    return Scrollbar(
      thumbVisibility: true,
      controller: scrollController,
      child: ListView.builder(
        controller: scrollController,
        itemCount: 100000,
        // itemExtent: 50.0,
        // itemExtentBuilder: (int index, SliverLayoutDimensions dimensions) {
        //   return dimensions.viewportMainAxisExtent / 10;
        // },
        // itemExtentBuilder: (int index, SliverLayoutDimensions dimensions) {
        //   if (index % 2 == 0) {
        //     return 50;
        //   } else {
        //     return 100;
        //   }
        // },
        itemBuilder: (BuildContext context, int index) {
          var color = Colors.yellow;
          if (index % 2 == 0) {
            color = Colors.red;
          }
          return ColoredBox(
            color: color,
            child: Center(
              child: Text('Item $index'),
            ),
          );
        },
      ),
    );
  }
}

class MyScrollBehavior extends MaterialScrollBehavior {
  @override
  Widget buildScrollbar(
      BuildContext context, Widget child, ScrollableDetails details) {
    return child;
  }

  @override
  Set<PointerDeviceKind> get dragDevices => <PointerDeviceKind>{
        PointerDeviceKind.touch,
        PointerDeviceKind.mouse,
      };
}

如上图所示,如果ListView不设置itemExtent,无法通过右侧的Scrollbar拖拽滑动,笔者运行环境为window PC,应用的主进程直接卡死,无法恢复。

当设置itemExtent之后,应用有着丝滑的性能:

目前,不同长度的item长列表的性能问题即将得到改善,[New feature] Allowing the ListView slivers to have different extents while still having scrolling performance,这个提交目前已经在Review阶段,相信很快会合入的master分支。

1 新特性-> ListView.itemBuilder

我们知道当前ListView可以通过ListView.itemExtent或者ListView.prototypeItem设置高度来提高Lazy Loading过程中的耗时,但这两个属性都是对所有items生效,如果items之间的高度不完全相同,对于长列表的性能问题还是挺严重的,在我们的业务的场景中,相信有很多这样的诉求。

PR131393提供一个新的属性itemExtentBuilder,有了它,我们可以为每一个item指定高度,同时有着丝滑的性能体验。

我们将上面demo修改一下:

dart 复制代码
child: ListView.builder(
  controller: scrollController,
  itemCount: 100000,
  itemExtentBuilder: (int index, SliverLayoutDimensions dimensions) {
    if (index % 2 == 0) {
      return 50;
    } else {
      return 100;
    }
  },
  itemBuilder: (BuildContext context, int index) {
    var color = Colors.yellow;
    if (index % 2 == 0) {
      color = Colors.red;
    }
    return ColoredBox(
      color: color,
      child: Center(
        child: Text('Item $index'),
      ),
    );
  },
)

这样,当items的高度并不完全一样的时候,同样有着丝滑的滚动性能:

我们来看下itemExtentBuilder的文档说明:

arduino 复制代码
/// {@template flutter.widgets.list_view.itemExtentBuilder}
/// If non-null, forces the children to have the corresponding extent returned
/// by the builder.
///
/// Specifying an [itemExtentBuilder] is more efficient than letting the children
/// determine their own extent because the scrolling machinery can make use of
/// the foreknowledge of the children's extent to save work, for example when
/// the scroll position changes drastically.
///
/// Unlike [itemExtent] or [prototypeItem], this allows children to have
/// different extents.
///
/// See also:
///
///  * [SliverExplicitExtentList], the sliver used internally when this property
///    is provided. It constrains its box children to have a specific given
///    extent along the main axis.
///  * The [itemExtent] property, which allows forcing the children's extent
///    to a given value.
///  * The [prototypeItem] property, which allows forcing the children's
///    extent to be the same as the given widget.
/// {@endtemplate}
final ItemExtentGetter? itemExtentBuilder;
java 复制代码
/// Called to get the item extent by the index of item.
typedef ItemExtentGetter = double Function(int index, SliverLayoutDimensions dimensions);

itemExtentBuilder是回调函数类型,入参为要获取高度的item的index索引,同时,还传递了SliverLayoutDimensions,我们看一下它里面有哪些信息:

kotlin 复制代码
/// Relates the dimensions of the [RenderSliver] during layout.
///
/// Used by [ListView.itemExtentBuilder] and [SliverExplicitExtentList.itemExtentBuilder].
@immutable
class SliverLayoutDimensions {
  /// Constructs a [SliverLayoutDimensions] with the specified parameters.
  const SliverLayoutDimensions({
    required this.scrollOffset,
    required this.precedingScrollExtent,
    required this.viewportMainAxisExtent,
    required this.crossAxisExtent
  });

  /// {@macro flutter.rendering.SliverConstraints.scrollOffset}
  final double scrollOffset;

  /// {@macro flutter.rendering.SliverConstraints.precedingScrollExtent}
  final double precedingScrollExtent;

  /// The number of pixels the viewport can display in the main axis.
  ///
  /// For a vertical list, this is the height of the viewport.
  final double viewportMainAxisExtent;

  /// The number of pixels in the cross-axis.
  ///
  /// For a vertical list, this is the width of the sliver.
  final double crossAxisExtent;

  @override
  bool operator ==(Object other) {
    if (identical(this, other)) {
      return true;
    }
    if (other is! SliverLayoutDimensions) {
      return false;
    }
    return other.scrollOffset == scrollOffset &&
      other.precedingScrollExtent == precedingScrollExtent &&
      other.viewportMainAxisExtent == viewportMainAxisExtent &&
      other.crossAxisExtent == crossAxisExtent;
  }

  @override
  String toString() {
    return 'scrollOffset: $scrollOffset'
      ' precedingScrollExtent: $precedingScrollExtent'
      ' viewportMainAxisExtent: $viewportMainAxisExtent'
      ' crossAxisExtent: $crossAxisExtent';
  }

SliverLayoutDimensions主要携带了四个信息:

  • scrollOffset:注意这里的offset是相对于当前Sliver的坐标系,指示当前最先可见的位置的偏移,例如,如果如果growthDirectionGrowthDirection.forward,且sliver位于初始位置,则该值为0
  • precedingScrollExtent:在当前Sliver之前已经被处理过的其它Sliver的长度,如果只有一个Sliver,则该值为0
  • viewportMainAxisExtentScrollable在主轴方向的Viewport的长度;
  • crossAxisExtent:在交叉轴方向的长度,对于垂直滚得控件来说,它是Sliver的宽度;

有了SliverLayoutDimensions参数之后,我们能够更灵活设置item的长度,例如,设置item固定为viewportMainAxisExtent的十分之一,也就是最多显示10个item:

perl 复制代码
itemExtentBuilder: (int index, SliverLayoutDimensions dimensions) {
  return dimensions.viewportMainAxisExtent / 10;

运行效果如下:

注意观察,当resize窗口的时候,列表里始终显示的是10条item,是不是很灵活呢?

2 总结

关于ListView.itemExtentBuilder我们就介绍到这里,有关它的技术实现源码,我们可以继续在PR 131393中交流吧,希望这个新特性能够对你的业务有所帮助:)

作者长期活跃在Flutter开源社区,欢迎大家一起参与开源社区的共建,如果您也有意愿参与Flutter社区的贡献,可以与作者联系。-->GITHUB

您也许还对这些Flutter技术分享感兴趣:

  1. 《社区说|从Flutter Key 深入剖析UI架构设计原理》
  2. 《社区说|Flutter 长列表 Lazy Loading 机制解析》
  3. 《【Flutter技术】Scrollbar实现原理解析》
  4. 《【Flutter技术】ScrollMetricsNotification的诞生记》
相关推荐
liulian09164 小时前
Flutter for OpenHarmony 跨平台开发:单位转换功能实战指南
flutter
千码君20164 小时前
Trae:一些关于flutter和 go前后端开发构建的分享
android·flutter·gradle·android-studio·trae·vibe code
maaath6 小时前
【maaath】Flutter for OpenHarmony 手表配饰应用实战开发
flutter·华为·harmonyos
maaath7 小时前
【maaath】Flutter for OpenHarmony 跨平台计算器应用开发实践
flutter·华为·harmonyos
maaath12 小时前
【maaath】Flutter for OpenHarmony 闹钟时钟应用开发实战
flutter·华为·harmonyos
maaath12 小时前
【maaath】Flutter for OpenHarmony 短信管理应用实战
flutter·华为·harmonyos
maaath13 小时前
【maaath】Flutter for OpenHarmony打造跨平台便签备忘录应用
flutter·华为·harmonyos
千码君201613 小时前
flutter:与Android Studio模拟器的调试分享
android·flutter
xmdy586614 小时前
Flutter+开源鸿蒙实战|智联邻里Day8 Lottie动画集成+url_launcher跳转拨号+个人中心完善+全局UI统一
flutter·开源·harmonyos
liulian09161 天前
Flutter for OpenHarmony 跨平台开发:颜色选择器功能实战指南
flutter