【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的坐标系,指示当前最先可见的位置的偏移,例如,如果如果growthDirection为GrowthDirection.forward,且sliver位于初始位置,则该值为0;
  • precedingScrollExtent:在当前Sliver之前已经被处理过的其它Sliver的长度,如果只有一个Sliver,则该值为0;
  • viewportMainAxisExtent:Scrollable在主轴方向的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的诞生记》
相关推荐
2601_949575864 小时前
JSON5 跨端三方库仓颉适配全流程
前端·javascript·flutter
法的空间4 小时前
AI 编程时代,为什么 Dart 分析会拖慢大型项目的开发
android·flutter·ios
恋猫de小郭6 小时前
Flutter 紧急发布 3.47.8 安全升级版本,建议都升级
android·前端·flutter
好的佩奇1 天前
Dart 之模式
android·flutter·dart
淡淡的香烟1 天前
Flutter实现多语言切换
android·flutter·1024程序员节
事圆则缓1 天前
从零搭建一套 Flutter 架构:先跑通数据流
flutter
程序员老刘2 天前
Flutter版本选择指南:3.44稳定了11周正式转正 | 2026年10月
flutter·ai编程·客户端
chicken_h2 天前
【无标题】
flutter·打包
事圆则缓2 天前
Flutter 底层渲染引擎怎么变了:从 Skia 到 Impeller,以及迁移时的坑
flutter
码林鼠3 天前
flutter状态管理入门教程-Riverpod
开发语言·javascript·flutter