Flutter笔记:聊一聊Flutter中委托的设计方法

简介: Flutter笔记:聊一聊Flutter中委托的设计方法

Flutter笔记聊一聊Flutter中委托的设计方法


1. 以 GridView 为例,从构造函数说起

1.1 默认构造函数的实现

GridView({
    super.key,
    super.scrollDirection,
    super.reverse,
    super.controller,
    super.primary,
    super.physics,
    super.shrinkWrap,
    super.padding,
    required this.gridDelegate,
    bool addAutomaticKeepAlives = true,
    bool addRepaintBoundaries = true,
    bool addSemanticIndexes = true,
    super.cacheExtent,
    List<Widget> children = const <Widget>[],
    int? semanticChildCount,
    super.dragStartBehavior,
    super.clipBehavior,
    super.keyboardDismissBehavior,
    super.restorationId,
  }) : childrenDelegate = SliverChildListDelegate(
         children,
         addAutomaticKeepAlives: addAutomaticKeepAlives,
         addRepaintBoundaries: addRepaintBoundaries,
         addSemanticIndexes: addSemanticIndexes,
       ),
       super(
         semanticChildCount: semanticChildCount ?? children.length,
       );

GridView 默认构造函数的结构可以看出,它本质上不过是创建了一个 SliverChildListDelegate 对象并赋值给 childrenDelegate

SliverChildListDelegateSliverChildDelegate 的一个实现,它 使用一个固定的子组件列表来生成网格的子组件。这里,children 参数就是这个列表。另外,addAutomaticKeepAlivesaddRepaintBoundariesaddSemanticIndexes 参数用于控制子组件的生命周期、是否添加重绘边界和语义索引。

另外一方面,该构造函数种调用了其父类(BoxScrollView)的构造函数。semanticChildCount 参数用于语义分析,它表示 GridView 中的子组件数量。如果 semanticChildCountnull,则使用 children.length 作为默认值。

1.2 GridView.builder构造函数

GridView.builder 构造函数用于创建一个可以滚动的,按需创建的二维部件数组。这对于具有大量(或无限)子部件的网格视图非常合适,因为构建器只会为实际可见的子部件调用。

/// 创建一个可滚动的,按需创建的二维部件数组。
  ///
  /// 对于具有大量(或无限)子部件的网格视图,此构造函数是合适的,因为构建器只会为实际可见的子部件调用。
  ///
  /// 提供非空的 `itemCount` 可以提高 [GridView] 估计最大滚动范围的能力。
  ///
  /// `itemBuilder` 只会被调用大于等于零且小于 `itemCount` 的索引。
  ///
  /// {@macro flutter.widgets.ListView.builder.itemBuilder}
  ///
  /// {@macro flutter.widgets.PageView.findChildIndexCallback}
  ///
  /// [gridDelegate] 参数是必需的。
  ///
  /// `addAutomaticKeepAlives` 参数对应于 [SliverChildBuilderDelegate.addAutomaticKeepAlives] 属性。
  /// `addRepaintBoundaries` 参数对应于 [SliverChildBuilderDelegate.addRepaintBoundaries] 属性。
  /// `addSemanticIndexes` 参数对应于 [SliverChildBuilderDelegate.addSemanticIndexes] 属性。
  GridView.builder({
    super.key,
    super.scrollDirection,
    super.reverse,
    super.controller,
    super.primary,
    super.physics,
    super.shrinkWrap,
    super.padding,
    required this.gridDelegate,
    required NullableIndexedWidgetBuilder itemBuilder,
    ChildIndexGetter? findChildIndexCallback,
    int? itemCount,
    bool addAutomaticKeepAlives = true,
    bool addRepaintBoundaries = true,
    bool addSemanticIndexes = true,
    super.cacheExtent,
    int? semanticChildCount,
    super.dragStartBehavior,
    super.keyboardDismissBehavior,
    super.restorationId,
    super.clipBehavior,
  }) : childrenDelegate = SliverChildBuilderDelegate(
         itemBuilder,
         findChildIndexCallback: findChildIndexCallback,
         childCount: itemCount,
         addAutomaticKeepAlives: addAutomaticKeepAlives,
         addRepaintBoundaries: addRepaintBoundaries,
         addSemanticIndexes: addSemanticIndexes,
       ),
       super(
         semanticChildCount: semanticChildCount ?? itemCount,
       );

从代码可以看到,这个构造函数接收多个参数,其中最重要的两个参数是 gridDelegateitemBuilder

  • gridDelegate 是一个 SliverGridDelegate 对象,它决定了网格的布局。这是一个必需的参数。
  • itemBuilder 是一个函数,它接收一个 BuildContext 和一个索引,然后返回一个 Widget。这个函数只会被调用大于等于零且小于 itemCount 的索引。这是一个必需的参数。

GridView.builder 构造函数的工作原理是,当需要渲染一个子部件时,它会调用 itemBuilder 函数,传入当前的 BuildContext 和子部件的索引,然后将返回的 组件 添加到网格中。这样,只有当子部件实际需要显示时,才会调用 itemBuilder 函数创建子部件。

此外,GridView.builder 还接收一些其他参数,如 itemCountaddAutomaticKeepAlivesaddRepaintBoundariesaddSemanticIndexes,这些参数用于控制 GridView 的行为。

最后,GridView.builder 通过 SliverChildBuilderDelegate 创建了一个 childrenDelegate,然后传递给 GridView 的父类构造函数。这个 childrenDelegate 决定了如何为 GridView 创建子部件。

1.3 GridView.custom构造函数

/// 使用自定义 [SliverGridDelegate] 和自定义 [SliverChildDelegate] 创建一个可滚动的二维部件数组。
  ///
  /// 要使用 [IndexedWidgetBuilder] 回调来构建子部件,可以使用 [SliverChildBuilderDelegate] 或使用 [GridView.builder] 构造函数。
  ///
  /// [gridDelegate] 和 [childrenDelegate] 参数不能为空。
  const GridView.custom({
    super.key,
    super.scrollDirection,
    super.reverse,
    super.controller,
    super.primary,
    super.physics,
    super.shrinkWrap,
    super.padding,
    required this.gridDelegate,
    required this.childrenDelegate,
    super.cacheExtent,
    super.semanticChildCount,
    super.dragStartBehavior,
    super.keyboardDismissBehavior,
    super.restorationId,
    super.clipBehavior,
  });

GridView.custom 构造函数用于创建一个可滚动的二维部件数组,它允许你完全自定义 **SliverGridDelegate **和 SliverChildDelegate

  • SliverGridDelegate 决定了网格的布局,例如每行的列数、每个子部件的尺寸等。
  • SliverChildDelegate 决定了如何生成网格的子部件。你可以使用 SliverChildBuilderDelegate 来按需生成子部件,或者使用 SliverChildListDelegate 来生成一个固定列表的子部件。

GridView.custom 构造函数接收多个参数,其中最重要的两个参数是 gridDelegate 和 childrenDelegate,这两个参数都是必需的。

  • gridDelegate 是一个 SliverGridDelegate 对象,它决定了网格的布局。
  • childrenDelegate 是一个 SliverChildDelegate 对象,它决定了如何为 GridView 创建子部件。

GridView.custom 会根据 gridDelegate 的设置来布局网格,然后调用 childrenDelegate 来生成子部件。这样,你可以完全自定义 GridView 的布局和子部件的生成方式。

在这个构造函数的实现种:

  • gridDelegate 实现网格的布局工作:gridDelegateSliverGridDelegate 类型的对象,它是一个委托,负责定义网格的布局。具体来说,它决定了网格中每行的列数,以及每个格子的大小。当 GridView 需要布局其子部件时,它会调用 gridDelegate 的方法来获取布局信息。所以,你可以说 gridDelegate 委托了网格的布局工作。
  • childrenDelegate 实现子部件的创建工作:childrenDelegateSliverChildDelegate 类型的对象,它是一个委托,负责创建网格的子部件。具体来说,当 GridView 需要渲染一个新的子部件时,它会调用 childrenDelegate 的方法来创建这个子部件。

1.4 GridView.count 构造函数

GridView.count 构造函数用于创建一个可滚动的二维部件数组,其中交叉轴上有固定数量的格子。这个构造函数接收多个参数,其中最重要的是 crossAxisCount,它决定了交叉轴上的格子数量。此外,还可以设置 mainAxisSpacing 和 crossAxisSpacing 来控制格子之间的间距,以及 childAspectRatio 来控制每个格子的宽高比。

该构造函数的代码为:

/// 创建一个可滚动的,二维部件数组,交叉轴上有固定数量的格子。
  ///
  /// 使用 [SliverGridDelegateWithFixedCrossAxisCount] 作为 [gridDelegate]。
  ///
  /// `addAutomaticKeepAlives` 参数对应于 [SliverChildListDelegate.addAutomaticKeepAlives] 属性。
  /// `addRepaintBoundaries` 参数对应于 [SliverChildListDelegate.addRepaintBoundaries] 属性。两者都不能为空。
  ///
  /// 另请参阅:
  ///
  ///  * [SliverGrid.count],[SliverGrid] 的等效构造函数。
  GridView.count({
    super.key,
    super.scrollDirection,
    super.reverse,
    super.controller,
    super.primary,
    super.physics,
    super.shrinkWrap,
    super.padding,
    required int crossAxisCount,
    double mainAxisSpacing = 0.0,
    double crossAxisSpacing = 0.0,
    double childAspectRatio = 1.0,
    bool addAutomaticKeepAlives = true,
    bool addRepaintBoundaries = true,
    bool addSemanticIndexes = true,
    super.cacheExtent,
    List<Widget> children = const <Widget>[],
    int? semanticChildCount,
    super.dragStartBehavior,
    super.keyboardDismissBehavior,
    super.restorationId,
    super.clipBehavior,
  }) : gridDelegate = SliverGridDelegateWithFixedCrossAxisCount(
         crossAxisCount: crossAxisCount,
         mainAxisSpacing: mainAxisSpacing,
         crossAxisSpacing: crossAxisSpacing,
         childAspectRatio: childAspectRatio,
       ),
       childrenDelegate = SliverChildListDelegate(
         children,
         addAutomaticKeepAlives: addAutomaticKeepAlives,
         addRepaintBoundaries: addRepaintBoundaries,
         addSemanticIndexes: addSemanticIndexes,
       ),
       super(
         semanticChildCount: semanticChildCount ?? children.length,
       );

从代码可以看出,GridView.count 构造函数会根据 crossAxisCountmainAxisSpacingcrossAxisSpacingchildAspectRatio 的值来布局网格,然后根据 children 列表来创建子部件。这使得你可以轻松地创建一个具有固定列数的网格视图。

GridView.count 构造函数中,gridDelegate 被设置为 SliverGridDelegateWithFixedCrossAxisCount 对象。这个对象会根据 crossAxisCountmainAxisSpacingcrossAxisSpacingchildAspectRatio 的值来布局网格。

childrenDelegate 被设置为 SliverChildListDelegate 对象,它会根据传入的 children 列表来创建子部件。addAutomaticKeepAlivesaddRepaintBoundariesaddSemanticIndexes 参数会传递给 SliverChildListDelegate,用于控制子部件的生命周期、是否添加重绘边界和语义索引。

1.5 GridView.extent 构造函数

GridView.extent 构造函数用于创建一个可滚动的二维部件数组,其中交叉轴上的每个格子都有最大的宽度。

这个构造函数接收多个参数,其中最重要的是 maxCrossAxisExtent,它决定了交叉轴上每个格子的最大宽度。此外,还可以设置 mainAxisSpacing 和 crossAxisSpacing 来控制格子之间的间距,以及 childAspectRatio 来控制每个格子的宽高比。

该构造函数源码为:

/// 创建一个可滚动的,二维部件数组,每个格子在交叉轴上都有最大的范围。
  ///
  /// 使用 [SliverGridDelegateWithMaxCrossAxisExtent] 作为 [gridDelegate]。
  ///
  /// `addAutomaticKeepAlives` 参数对应于 [SliverChildListDelegate.addAutomaticKeepAlives] 属性。
  /// `addRepaintBoundaries` 参数对应于 [SliverChildListDelegate.addRepaintBoundaries] 属性。两者都不能为空。
  ///
  /// 另请参阅:
  ///
  ///  * [SliverGrid.extent],[SliverGrid] 的等效构造函数。
  GridView.extent({
    super.key,
    super.scrollDirection,
    super.reverse,
    super.controller,
    super.primary,
    super.physics,
    super.shrinkWrap,
    super.padding,
    required double maxCrossAxisExtent,
    double mainAxisSpacing = 0.0,
    double crossAxisSpacing = 0.0,
    double childAspectRatio = 1.0,
    bool addAutomaticKeepAlives = true,
    bool addRepaintBoundaries = true,
    bool addSemanticIndexes = true,
    super.cacheExtent,
    List<Widget> children = const <Widget>[],
    int? semanticChildCount,
    super.dragStartBehavior,
    super.keyboardDismissBehavior,
    super.restorationId,
    super.clipBehavior,
  }) : gridDelegate = SliverGridDelegateWithMaxCrossAxisExtent(
         maxCrossAxisExtent: maxCrossAxisExtent,
         mainAxisSpacing: mainAxisSpacing,
         crossAxisSpacing: crossAxisSpacing,
         childAspectRatio: childAspectRatio,
       ),
       childrenDelegate = SliverChildListDelegate(
         children,
         addAutomaticKeepAlives: addAutomaticKeepAlives,
         addRepaintBoundaries: addRepaintBoundaries,
         addSemanticIndexes: addSemanticIndexes,
       ),
       super(
         semanticChildCount: semanticChildCount ?? children.length,
       );

GridView.extent 构造函数会根据 maxCrossAxisExtentmainAxisSpacingcrossAxisSpacingchildAspectRatio 的值来布局网格,然后根据 children 列表来创建子部件。这使得你可以轻松地创建一个具有固定最大宽度的网格视图。

GridView.extent 构造函数中,gridDelegate 被设置为 SliverGridDelegateWithMaxCrossAxisExtent 对象。这个对象会根据 maxCrossAxisExtentmainAxisSpacingcrossAxisSpacingchildAspectRatio 的值来布局网格。

childrenDelegate 被设置为 SliverChildListDelegate 对象,它会根据传入的 children 列表来创建子部件。addAutomaticKeepAlivesaddRepaintBoundariesaddSemanticIndexes 参数会传递给 SliverChildListDelegate,用于控制子部件的生命周期、是否添加重绘边界和语义索引。

2. 什么是委托

2.1 Flutter中委托类的机制

Flutter中,“Delegate” 结尾的类通常表示这些类的主要目的是"委托"(Delegate)某些工作或责任给其他类或者提供了一种扩展或自定义的机制。 —— 这个命名约定的目的是提醒开发者这些类通常用于 委托某种功能提供可插拔性

具体来说,以下是一些常见情况,“Delegate” 结尾的类可能委托了什么:

  1. 业务逻辑委托:某些 “Delegate” 类可能用于将特定的业务逻辑委托给其他类。例如,一个 ListView.builder 中的 itemBuilder 参数允许你委托生成列表项的逻辑给开发者自定义的函数。
  2. 路由过渡委托:一些类用于定义页面路由之间的过渡效果,比如 PageRouteBuilder,允许开发者委托定义过渡效果的工作。
  3. 渲染和布局委托:在渲染和布局阶段,一些 “Delegate” 类允许你自定义渲染对象的外观和布局,除了这里介绍的 GridView使用两类 Delegate 分别实现提供子组件和子组件布局外——再比如 SliverPersistentHeaderDelegate 用于定义 SliverAppBar 的外观和行为。
  4. 插件或扩展机制:一些 “Delegate” 类可能用于实现插件或扩展的接口,这允许开发者自定义、扩展或替换某些功能的实现。
  5. 策略模式的委托:某些 “Delegate” 类实际上是策略模式的实现,允许你根据需要在不同的算法或行为之间切换,而不需要修改客户端代码。

Delegate” 结尾的类通常表示它们起到了一种"桥梁"或"中介"的作用,将某些具体的实现委托给其他类,以实现灵活性、可定制性和可扩展性。这种命名约定有助于在代码中识别和理解这些类的用途和作用。

2.1 设计原则与模式

Flutter中以 “Delegate” 结尾的这些类通常,以分离不同部分之间的关注点并提供更好的可维护性和可扩展性。这些 “Delegate” 类通常遵循某些规则和设计原则,如以下所示:

  1. 分离关注点:“Delegate” 类有助于将代码拆分为不同的部分,每个部分关注特定的责任。这有助于减少代码的复杂性,提高可读性和可维护性。
  2. 单一责任原则(SRP):“Delegate” 类通常遵循 SRP,每个类关注一个单一的责任。这使得代码更容易测试、理解和维护。
  3. 策略模式:在许多情况下, “Delegate” 类实现策略模式,允许您根据需要在不同的算法或行为之间切换。这种设计模式有助于动态地选择不同的实现,而不需要修改客户端代码。
  4. 插件机制:许多 Delegate 类用于实现插件或扩展的接口,以便轻松添加和扩展功能。这样,开发人员可以创建自定义实现以满足其需求,而不必修改核心代码。

这些 Delegate 类的共同特点是它们提供了灵活性、可扩展性和可定制性,使得 Flutter· 应用程序可以更容易地满足不同的需求。通过使用这些类,开发人员可以更轻松地实现一些通用的设计原则,例如单一责任原则和策略模式,以改进应用程序的结构和性能。

3. 再看 GridView

3.1 回顾

本文 第1小节 介绍了 GridView 的用法。 GridView 有多种构造函数,其中一些构造函数使用了委托(Delegate)的概念,例如 GridView.builderGridView.custom,现在以这两个构造函数简单回顾一下。


  1. GridView.builder:这个构造函数允许你动态地创建网格项目。它需要一个 GridDelegate 和一个 IndexedWidgetBuilderGridDelegate 用于控制网格的布局,IndexedWidgetBuilder 用于生成网格项目。其中:
  • GridDelegate是一个抽象类,它有多个实现,例如:SliverGridDelegateWithFixedCrossAxisCountSliverGridDelegateWithMaxCrossAxisExtent。这些实现类允许你自定义网格的布局,例如设置交叉轴的项目数量或最大尺寸。
  • IndexedWidgetBuilder 是一个回调函数,它接收一个上下文和一个索引,返回一个新的组件。你可以在这个回调函数中根据索引生成不同的组件。

  1. GridView.custom:这个构造函数允许你使用自定义的SliverChildDelegate来生成网格项目。其中:
  • SliverChildDelegate 是一个抽象类,它有多个实现,例如SliverChildBuilderDelegateSliverChildListDelegate。这些实现类允许你自定义网格项目的生成方式,例如使用一个回调函数或一个固定的小组件列表。

在这两个构造函数中,GridView 组件都使用了 Delegate 的类来提供灵活性和可扩展性。

通过使用这些类,实现定义网格的布局和项目的生成方式,而不需要修改 GridView 组件的源代码。

这应该算是一种的 策略模式 的应用,它 允许你在不同的策略之间动态地切换,以满足不同的需求

进一步看 GridView 的代码,还需要在先介绍 GridViewgridDelegate 属性 和 childrenDelegate 属性以后,最后再看负责构建 GridView 的子布局的 buildChildLayout 方法。

3.2 SliverGrid 组件的默认构造函数

3.2.2 SliverChild 默认构造函数

const SliverGrid({
  super.key,
  required super.delegate,
  required this.gridDelegate,
});
  • delegate:这是一个必需参数,类型为SliverChildDelegate。它负责提供和构建子组件。而其中SliverChildDelegate包括:
  • SliverChildListDelegate(用于处理 固定数量 的子组件)
  • SliverChildBuilderDelegate(用于处理懒加载的子组件)。
  • gridDelegate:这也是一个必需参数,类型为SliverGridDelegate。它负责控制SliverGrid的布局,例如每行的列数,子部件的长宽比,子部件之间的间距等。常见的SliverGridDelegate包括:
  • SliverGridDelegateWithFixedCrossAxisCount(固定列数)
  • SliverGridDelegateWithMaxCrossAxisExtent(最大交叉轴范围)。

3.2.2 SliverChildDelegate

SliverChildDelegate 是为 slivers 提供子组件的 抽象类

许多 slivers 会懒加载它们的 box 子组件,以避免创建比视口可见的子组件更多。它们不是通过显式的 List 接收子组件,而是通过 SliverChildDelegate 接收子组件。

【比较】

SliverChildBuilderDelegate 是一个使用 构建器回调 来构造子组件的委托

SliverChildListDelegate 是一个具有显式子列表的委托。

子元素的生命周期阶段

子元素的生命周期包括 创建(Creation)、销毁(Destruction) 和 销毁缓和(Destruction mitigation)三个阶段:

(1)创建(Creation)阶段

创建(Creation) 阶段,可见子组件的元素、状态和渲染对象将基于现有组件。

SliverChildListDelegate 的情况 或 懒加载提供的组件(如 SliverChildBuilderDelegate 的情况)懒加载创建。

(2)销毁阶段

销毁阶段,当一个子组件滚动出视图时,关联的元素子树、状态和渲染对象被销毁。

当它滚动回来时,一个新的子组件在 sliver 的同一位置将懒加载重新创建,同时新的元素、状态和渲染对象也会被创建。

(3)销毁缓和(Destruction mitigation)阶段

销毁缓和(Destruction mitigation) 阶段,为了在子元素滚动进出视图时 保留状态,有以下可能的选项:

  1. 将非琐碎的驱动 UI 状态的业务逻辑的所有权移出 sliver 子组件子树;
  2. KeepAlive 成为需要保留的 sliver 子组件子树的根组件;
  3. 使用 AutomaticKeepAlive 组件。

如果在单个滚动视图中使用多个委托,每个委托的第一个子组件总是会被布局,即使它超出了当前可视区域。这是因为至少需要一个子组件来为整个滚动视图 estimateMaxScrollOffset,因为它使用当前构建的子组件来估计剩余子组件的范围。


(1) SliverChildListDelegate extends SliverChildDelegate

SliverChildListDelegate 是一个用于为 slivers 提供子组件的 委托Delegate,它使用一个明确的列表来生成子组件。这种方式很方便,但如果子组件的数量很大,或者子组件的创建成本很高,使用 SliverChildBuilderDelegate 或直接继承 SliverChildDelegate 来实现懒加载可能会更有效率。

(2) SliverChildBuilderDelegate extends SliverChildDelegate

SliverChildBuilderDelegate 是一个用于为 slivers 提供子组件的委托Delegate,它使用一个构建器回调来按需生成子组件。这种方式允许子组件进行懒加载,即只有当子组件真正需要显示时才会创建。这对于子组件数量大或者创建成本高的情况非常有用,因为它可以帮助我们避免不必要的资源浪费。此外,SliverChildBuilderDelegate 还提供了一些额外的控制,如是否为每个子组件添加自动保活(AutomaticKeepAlive)、重绘边界(RepaintBoundary)以及语义索引(SemanticIndex)。

3.2.3 SliverGridDelegate

(1) SliverGridDelegateWithFixedCrossAxisCount extends SliverGridDelegate

SliverGridDelegateWithFixedCrossAxisCount 是一个用于为 slivers 提供网格布局的委托Delegate,它在交叉轴上具有固定数量的单元格。

=> 这意味着无论网格的总体大小如何,交叉轴上的单元格数量始终保持不变。这对于需要创建均匀分布的网格布局(例如图片网格)非常有用。此外,你还可以控制单元格之间的间距(mainAxisSpacingcrossAxisSpacing)、单元格的宽高比(childAspectRatio)以及网格的内边距(padding)。

(2) SliverGridDelegateWithMaxCrossAxisExtent extends SliverGridDelegate

SliverGridDelegateWithMaxCrossAxisExtent 是一个用于为 slivers 提供网格布局的委托Delegate,它在交叉轴上的单元格具有最大扩展尺寸。

=> 这意味着单元格的尺寸会自动调整以适应网格的总体大小,但不会超过设定的最大尺寸。这对于需要创建响应式布局的网格非常有用,因为你可以根据屏幕的大小自动调整单元格的数量。此外,你还可以控制单元格之间的间距(mainAxisSpacingcrossAxisSpacing)、单元格的宽高比(childAspectRatio)以及网格的内边距(padding)。

3.3 GridView 的 gridDelegate属性

gridDelegate 是 GridView 类中的一个属性,它的类型是 SliverGridDelegate。这个属性是一个委托(delegate),它决定了 GridView 中子部件的布局。

其源代码为:

/// 一个委托,控制 [GridView] 中子部件的布局。
  ///
  /// [GridView],[GridView.builder] 和 [GridView.custom] 构造函数允许你明确指定这个委托。其他构造函数隐式创建一个 [gridDelegate]。
  final SliverGridDelegate gridDelegate;

gridDelegate 属性的作用就是定义 GridView 中子部件的布局。这使得 GridView 可以灵活地适应各种需求,例如创建固定列数的网格,或者创建具有固定最大宽度的网格。

SliverGridDelegate 是一个抽象类,它有两个常用的子类:SliverGridDelegateWithFixedCrossAxisCountSliverGridDelegateWithMaxCrossAxisExtent

  • SliverGridDelegateWithFixedCrossAxisCount 创建一个网格,其中交叉轴上有固定数量的格子。你可以指定交叉轴上的格子数量,以及格子之间的间距和宽高比。
  • SliverGridDelegateWithMaxCrossAxisExtent 创建一个网格,其中交叉轴上的每个格子都有最大的宽度。你可以指定每个格子的最大宽度,以及格子之间的间距和宽高比。

GridViewGridView.builderGridView.custom 构造函数中,你可以明确指定 gridDelegate。在其他构造函数中,gridDelegate 会自动创建。

3.4 GridView 的 childrenDelegate属性

/// 一个委托,为 [GridView] 提供子部件。
  ///
  /// [GridView.custom] 构造函数允许你明确指定这个委托。其他构造函数创建一个包装给定子部件列表的 [childrenDelegate]。
  final SliverChildDelegate childrenDelegate;

可以看到,childrenDelegate 属性类型为 SliverChildDelegate。这个属性是一个 委托(delegate),它决定了如何为 GridView 创建子部件。

SliverChildDelegate 是一个抽象类,它有两个常用的子类:SliverChildListDelegateSliverChildBuilderDelegate。其中:

  • SliverChildListDelegate 接收一个固定长度的子部件列表,然后按照列表顺序创建子部件。
  • SliverChildBuilderDelegate 接收一个构建函数,然后按需创建子部件。这对于具有大量子部件的 GridView 非常有用,因为只有当子部件实际需要显示时,才会调用构建函数创建子部件。

GridView.custom 构造函数中,你可以明确指定 childrenDelegate。在其他构造函数中,childrenDelegate 会自动创建,通常是包装给定的子部件列表。

因此,childrenDelegate 属性的作用就是定义如何为 GridView 创建子部件。这使得 GridView 可以灵活地适应各种需求,例如创建固定数量的子部件,或者按需创建子部件。

3.5 GridView 的 buildChildLayout 方法

buildChildLayout 负责构建 GridView 的子布局。

buildChildLayout 方法是 GridView 的核心,它负责构建 GridView 的子布局。这个方法的实现非常简单,只是创建了一个 SliverGrid 对象,并将 childrenDelegate 和 gridDelegate 传递给了它。它的代码如下:

@override
  Widget buildChildLayout(BuildContext context) {
    return SliverGrid(
      delegate: childrenDelegate, // SliverChildDelegate 对象,它决定了如何创建和布局子项。
      gridDelegate: gridDelegate, // SliverGridDelegate 对象,它决定了网格的布局。
    );
  }

可以看到,负责构建 GridView 的子布局的 实际上是通过 SliverGrid实现的,其中需要指定两个信息:

  1. 如何创建和布局子项:delegate参数;
    SliverGriddelegate 参数类型为 SliverChildDelegate,它负责提供和构建子部件。childrenDelegate 是传递给 delegate 的参数,它可能是 SliverChildListDelegate(用于处理固定数量的子部件)或 SliverChildBuilderDelegate(用于处理懒加载的子部件)。
  2. 网格的布局:gridDelegate参数。

3.6 这就是Flutter设计中的 委托

GridViewbuildChildLayout 方法中,我们可以看到委托(Delegate)的强大之处。

GridView 本身并没有实现如何创建和布局子项,以及如何布局网格,而是将这些工作委托给了 childrenDelegate 和 gridDelegate。

childrenDelegate 是一个 SliverChildDelegate 对象,它决定了如何创建和布局子项。这个委托可以是 SliverChildListDelegate(用于处理固定数量的子部件)或 SliverChildBuilderDelegate(用于处理懒加载的子部件)。通过这个委托,GridView 可以灵活地创建和布局子项,无论是固定数量的子部件,还是懒加载的子部件。

gridDelegate 是一个 SliverGridDelegate 对象,它决定了网格的布局。这个委托可以是 SliverGridDelegateWithFixedCrossAxisCount(固定列数的网格)或 SliverGridDelegateWithMaxCrossAxisExtent(最大交叉轴范围的网格)。通过这个委托,GridView 可以灵活地布局网格,无论是固定列数的网格,还是最大交叉轴范围的网格。

总的来说,通过委托(Delegate)的方式,GridView 将创建和布局子项,以及布局网格的工作交给了其他对象,从而实现了代码的解耦和复用。

4. 小结与启示

Flutter 中,委托(Delegate)类 可以认为是一种 设计模式,它允许一个类将某些任务或责任委托给另一个类。这种设计模式可以提高代码的 灵活性可复用性,因为它允许我们在不修改原有类的情况下,改变或扩展某些功能。

GridView 的例子中,我们看到了两种类型的委托:SliverChildDelegateSliverGridDelegate。SliverChildDelegate 负责创建和布局子项,而 SliverGridDelegate 负责布局网格。通过这两个委托,GridView 可以灵活地创建和布局子项,以及布局网格,而无需自己实现这些功能。

这种设计模式的优点是:

  • 分离关注点:委托允许我们将不同的功能分离到不同的类中,每个类只关注一个特定的任务或责任。这有助于减少代码的复杂性,提高可读性和可维护性。
  • 提高复用性:通过委托,我们可以在不同的上下文中复用同一个类。例如,我们可以在不同的 GridView 中复用同一个 SliverChildDelegate 或 SliverGridDelegate。
  • 提高灵活性:通过委托,我们可以在运行时改变某些功能的实现。例如,我们可以在运行时改变 GridView 的 gridDelegate,从而改变网格的布局。

这种模式可以应用于以后的编程中。例如,假设我们正在设计一个自定义的列表组件 MyListView,我们可以设计两个委托:ItemBuilderItemLayoutDelegateItemBuilder 负责创建列表项,ItemLayoutDelegate 负责布局列表项。

class MyListView {
  final ItemBuilder itemBuilder;
  final ItemLayoutDelegate itemLayoutDelegate;
  MyListView({required this.itemBuilder, required this.itemLayoutDelegate});
  // ...
}
typedef ItemBuilder = Widget Function(BuildContext context, int index);
abstract class ItemLayoutDelegate {
  double getItemSize(int index);
  // ...
}

通过这种方式,我们可以在不修改 CustomListView 的情况下,改变列表项的创建方式和布局方式,只需要提供不同的 ItemBuilder 和 ItemLayoutDelegate 即可。这就是委托(Delegate)设计模式的强大之处。

这种委托(Delegate)编程模式在 Flutter 中得到了广泛的应用。通过理解和掌握这种设计模式,我们可以更好地理解和使用 Flutter,以及编写出更灵活、更可复用的代码。

附录

F.1 Flutter中的一些以Delegate结尾的类(表)

以下是类名和它们的描述的表格形式:

类名 描述
DefaultPlatformMenuDelegate 用于创建默认平台菜单的委托,定制菜单外观和行为。
DesktopTextSelectionToolbarLayoutDelegate 自定义桌面端文本选择工具栏布局和外观的委托。
DiagnosticsSerializationDelegate 支持诊断信息的序列化和反序列化,用于查看应用程序状态和结构。
FlowDelegate 定制Flow布局的委托,用于排列子组件。
InspectorSerializationDelegate 支持Flutter开发工具中检查器信息的序列化和反序列化。
ListWheelChildBuilderDelegate 构建滚动选择器(ListWheelScrollView)的子项的委托。
ListWheelChildListDelegate 传递子项列表给滚动选择器的委托。
ListWheelChildLoopingListDelegate 支持无限循环滚动选择器的委托。
LocalizationsDelegate 本地化国际化支持的委托,切换不同本地化语言。
MultiChildLayoutDelegate 自定义多子组件布局的委托,与MultiChildLayout组件一起使用。
MultiSelectableSelectionContainerDelegate 自定义多选择容器的委托,支持选择多个子组件。
PlatformMenuDelegate 创建平台菜单的委托,帮助创建一致的菜单。
RouterDelegate 导航路由的委托,管理Flutter应用的导航栈和路由。
ScrollActivityDelegate 自定义滚动活动的委托,处理滚动活动事件。
ScrollDragController 控制拖拽滚动的委托,控制滚动速度和行为。
SearchDelegate 创建搜索界面和处理搜索操作的委托,与搜索组件一起使用。
SelectionContainer 支持文本选择和复制操作的容器,允许选择和复制文本。
SemanticsConfiguration 定义语义信息的配置,帮助无障碍用户理解和操作应用程序。
SemanticsGestureDelegate 自定义语义手势的委托,支持无障碍用户的交互。
ShadowRoot 表示DOM元素的Shadow DOM根,通常与Web视图集成一起使用。
ShadowRootExtension 扩展Shadow DOM的委托,支持自定义Shadow DOM的行为。
SingleChildLayoutDelegate 自定义单子组件布局的委托,与SingleChildLayout组件一起使用。
SingleSelectableSelectionContainerDelegate 支持单一选择的容器的委托,与单一选择容器一起使用。
SliverChildBuilderDelegate 构建Sliver子组件的委托,通常与Sliver列表一起使用。
SliverChildListDelegate 将Sliver子组件作为列表传递给Sliver列表的委托。
SliverFillViewport 填充Sliver视口的委托,在Sliver列表中的某些情况下使用。
SliverGridDelegate 定义Sliver网格布局的委托,通常与Sliver网格列表一起使用。
SliverGridDelegateWithFixedCrossAxisCount 定义具有固定交叉轴方向子项数量的Sliver网格布局的委托。
SliverGridDelegateWithMaxCrossAxisExtent 定义具有最大交叉轴方向子项尺寸的Sliver网格布局的委托。
SliverMultiBoxAdaptorWidget 在Sliver列表中包装多个子组件的组件。
SliverPersistentHeader 创建Sliver列表中的持久性标题的组件。
SliverPersistentHeaderDelegate 定义Sliver列表中持久性标题的委托。
SpellCheckSuggestionsToolbarLayoutDelegate 自定义拼写检查建议工具栏的布局和外观的委托。
TestDefaultBinaryMessenger 用于测试目的的默认二进制消息传递器,通常不用于生产环境。
TextSelectionGestureDetectorBuilderDelegate 构建文本选择手势检测器的委托,通常与文本选择操作相关。
TextSelectionToolbarLayoutDelegate 自定义文本选择工具栏的布局和外观的委托。
TransitionDelegate 自定义页面路由转场效果的委托,创建自定义的页面切换动画。
TwoDimensionalChildBuilderDelegate 构建二维滚动视图的子组件的委托。
TwoDimensionalChildDelegate 二维滚动视图的子组件委托,支持定制子组件的位置和行为。
TwoDimensionalChildListDelegate 在二维滚动视图中传递子项列表的委托。
TwoDimensionalScrollView 用于支持二维滚动的滚动视图的委托。
ZoneDelegate 用于Zone委托,通常用于资源管理和隔离。

F.2 SliverChildDelegate源码

/// 为 slivers 提供子组件的委托。
///
/// 许多 slivers 会懒加载它们的 box 子组件,以避免创建比视口可见的子组件更多。
/// 它们不是通过显式的 [List] 接收子组件,而是通过 [SliverChildDelegate] 接收子组件。
///
/// 子类化 [SliverChildDelegate] 是不常见的。相反,考虑使用已有的子类,
/// 它们提供了适配器到构建器回调或显式子列表。
///
/// {@template flutter.widgets.SliverChildDelegate.lifecycle}
/// ## 子元素的生命周期
///
/// ### 创建
///
/// 在布局列表时,可见子组件的元素、状态和渲染对象将基于现有组件(如 [SliverChildListDelegate] 的情况)
/// 或懒加载提供的组件(如 [SliverChildBuilderDelegate] 的情况)懒加载创建。
///
/// ### 销毁
///
/// 当一个子组件滚动出视图时,关联的元素子树、状态和渲染对象被销毁。
/// 当它滚动回来时,一个新的子组件在 sliver 的同一位置将懒加载重新创建,
/// 同时新的元素、状态和渲染对象也会被创建。
///
/// ### 减轻销毁
///
/// 为了在子元素滚动进出视图时保留状态,有以下可能的选项:
///
///  * 将非琐碎的驱动 UI 状态的业务逻辑的所有权移出 sliver 子组件子树。
///    例如,如果列表包含帖子,它们的点赞数来自缓存的网络响应,
///    将帖子列表和点赞数存储在列表外的数据模型中。让 sliver 子组件 UI 子树
///    能够从真实源模型对象轻松重新创建。在子组件子树中使用 [StatefulWidget] 
///    仅存储瞬时 UI 状态。
///
///  * 让 [KeepAlive] 成为需要保留的 sliver 子组件子树的根组件。
///    [KeepAlive] 组件将子组件子树的顶部渲染对象子组件标记为 keepalive。
///    当关联的顶部渲染对象滚动出视图时,sliver 将子组件的渲染对象(以及相应的元素和状态)
///    保留在缓存列表中,而不是销毁它们。当滚动回视图时,渲染对象会按原样重绘
///    (如果在此期间没有被标记为 dirty)。
///
///    这只有在 [SliverChildDelegate] 的子类不通过 `addAutomaticKeepAlives` 和
///    `addRepaintBoundaries` 将其他组件(如 [AutomaticKeepAlive] 和 [RepaintBoundary])包装到
///    子组件子树中时才有效。
///
///  * 使用 [AutomaticKeepAlive] 组件(默认插入到 [SliverChildListDelegate] 或 [SliverChildListDelegate] 中)。
///    [AutomaticKeepAlive] 允许后代组件控制子树是否真的保持活动状态。这种行为与
///    [KeepAlive] 形成对比,后者将无条件地保持子树活动状态。
///
///    例如,[EditableText] 组件在其文本字段有输入焦点时,会发出信号让其 sliver 
///    组件在其文本字段有输入焦点时,会发出信号让其 sliver 子元素子树保持活动状态。
///    如果它没有焦点,且没有其他后代通过 [KeepAliveNotification] 发出保持活动的信号,
///    当滚动离开视图时,sliver 子元素子树将被销毁。
///
///    [AutomaticKeepAlive] 的后代通常通过使用 [AutomaticKeepAliveClientMixin],
///    然后实现 [AutomaticKeepAliveClientMixin.wantKeepAlive] getter 和调用
///    [AutomaticKeepAliveClientMixin.updateKeepAlive] 来发出保持活动的信号。
///
/// ## 在 [Viewport] 中使用多个委托
///
/// 如果在单个滚动视图中使用多个委托,每个委托的第一个子组件总是会被布局,
/// 即使它超出了当前可视区域。这是因为至少需要一个子组件来为整个滚动视图
/// [estimateMaxScrollOffset],因为它使用当前构建的子组件来估计剩余子组件的范围。
/// {@endtemplate}
///
/// 另请参阅:
///
///  * [SliverChildBuilderDelegate],这是一个使用构建器回调来构造子组件的委托。
///  * [SliverChildListDelegate],这是一个具有显式子列表的委托。
abstract class SliverChildDelegate {
  /// 抽象的 const 构造函数。这个构造函数使子类能够提供 const 构造函数,
  /// 以便它们可以在 const 表达式中使用。
  const SliverChildDelegate();
  /// 返回给定索引的子组件。
  ///
  /// 如果被要求构建一个大于存在的索引的组件,应返回 null。如果这返回 null,
  /// [estimatedChildCount] 必须随后返回一个精确的非 null 值
  /// (然后用于实现 [RenderSliverBoxChildManager.childCount])。
  ///
  /// 子类通常覆盖此函数,并将其子组件包装在 [AutomaticKeepAlive]、[IndexedSemantics] 和
  /// [RepaintBoundary] 组件中。
  ///
  /// 此方法返回的值被缓存。要表示组件已更改,必须提供一个新的委托,
  /// 并且新委托的 [shouldRebuild] 方法必须返回 true。
  Widget? build(BuildContext context, int index);
  /// 返回此委托将构建的子组件数量的估计值。
  ///
  /// 用于估计最大滚动偏移量,如果 [estimateMaxScrollOffset] 返回 null。
  ///
  /// 如果有无限多的子组件,或者估计子组件数量太困难,返回 null。
  ///
  /// 一旦 [build] 返回 null,这必须返回一个精确的数字,
  /// 因为它用于实现 [RenderSliverBoxChildManager.childCount]。
  int? get estimatedChildCount => null;
  /// 返回所有子组件的最大滚动范围的估计值。
  ///
  /// 如果子类有关于其最大滚动范围的额外信息,应覆盖此函数。
  ///
  /// 默认实现返回 null,这会导致调用者从给定参数中推断最大滚动偏移量。
  double? estimateMaxScrollOffset(
    int firstIndex,
    int lastIndex,
    double leadingScrollOffset,
    double trailingScrollOffset,
  ) => null;
  /// 在布局结束时调用,表示现在布局已完成。
  ///
  /// `firstIndex` 参数是在当前布局中包含的第一个子组件的索引。
  /// `lastIndex` 参数是在当前布局中包含的最后一个子组件的索引。
  ///
  /// 对于希望跟踪哪些子组件包含在底层渲染树中的子类很有用。
  void didFinishLayout(int firstIndex, int lastIndex) { }
  /// 每当向 sliver 提供子委托类的新实例时调用。
  ///
  /// 如果新实例代表的信息与旧实例不同,则该方法应返回 true,否则应返回 false。
  ///
  /// 如果该方法返回 false,则可能会优化掉 [build] 调用。
  bool shouldRebuild(covariant SliverChildDelegate oldDelegate);
  /// 查找与关联键的子元素的索引。
  ///
  /// 在 [SliverMultiBoxAdaptorElement] 的 `performRebuild` 中调用,
  /// 以检查子组件是否移动到了不同的位置。它应返回与关联键的子元素的索引,如果未找到,返回 null。
  ///
  /// 如果未提供,当从子组件构建器返回的子组件顺序改变时,
  /// 子组件可能不会映射到其现有的 [RenderObject]。这可能导致状态丢失。
  int? findIndexByKey(Key key) => null;
  @override
  String toString() {
    final List<String> description = <String>[];
    debugFillDescription(description);
    return '${describeIdentity(this)}(${description.join(", ")})';
  }
   /// 为 [toString] 使用,向给定的描述中添加额外的信息。
  @protected
  @mustCallSuper
  void debugFillDescription(List<String> description) {
    try {
      final int? children = estimatedChildCount;
      if (children != null) {
        description.add('estimated child count: $children');
      }
    } catch (e) {
      // 异常被转发到组件检查器。
      description.add('estimated child count: EXCEPTION (${e.runtimeType})');
    }
  }
}

F.3 SliverChildListDelegate源码

/// 使用明确列表为 slivers 提供子组件的委托。
///
/// 许多 slivers 会懒加载它们的 box 子组件,以避免创建比视口可见的子组件更多。
/// 这个委托使用一个明确的列表来提供子组件,这种方式很方便,但减少了懒加载子组件的优势。
///
/// 通常来说,预先构建所有的组件并不高效。更好的方式是使用 [SliverChildBuilderDelegate] 
/// 或直接继承 [SliverChildDelegate] 来创建一个可以按需构建组件的委托。
///
/// 这个类适用于以下情况:子组件列表在很早之前就已知(理想情况下,子组件本身就是编译时常量),
/// 因此每次创建委托时都不需要构建子组件,或者列表很小,很可能始终可见(因此没有必要按需构建)。
/// 例如,对话框的主体可能符合这两种情况。
///
/// 如果 [addAutomaticKeepAlives] 为 true(默认值),则给定 [children] 列表中的组件会自动包装在
/// [AutomaticKeepAlive] 组件中。如果 [addRepaintBoundaries] 为 true(也是默认值),则会包装在
/// [RepaintBoundary] 组件中。
///
/// ## 辅助功能
///
/// [CustomScrollView] 要求其语义子组件使用 [IndexedSemantics] 进行注释。
/// 默认情况下,委托会将 `addSemanticIndexes` 参数设置为 true 来完成这项工作。
///
/// 如果在单个滚动视图中使用了多个委托,那么默认情况下索引将不正确。
/// 可以使用 `semanticIndexOffset` 来偏移每个委托的语义索引,使索引单调递增。
/// 例如,如果滚动视图包含两个委托,第一个委托有 10 个子组件贡献语义,那么第二个委托应该将其子组件偏移 10。
///
/// 在某些情况下,只有子组件的一个子集应该用语义索引进行注释。
/// 例如,在 [ListView.separated()] 中,分隔符没有与之关联的索引。
/// 这是通过提供一个 `semanticIndexCallback` 来完成的,它为分隔符索引返回 null,
/// 并将非分隔符索引向下取整到一半。
///
/// 参见 [SliverChildBuilderDelegate],了解使用 `semanticIndexOffset` 和 `semanticIndexCallback` 的示例代码。
///
/// 另请参见:
///
///  * [SliverChildBuilderDelegate],这是一个使用构建器回调来构建子组件的委托。
class SliverChildListDelegate extends SliverChildDelegate {
  /// 使用给定列表为 slivers 提供子组件的委托。
  ///
  /// [children]、[addAutomaticKeepAlives]、[addRepaintBoundaries]、
  /// [addSemanticIndexes] 和 [semanticIndexCallback] 参数必须非 null。
  ///
  /// 如果子组件的顺序永远不会改变,考虑使用常量 [SliverChildListDelegate.fixed] 构造函数。
  SliverChildListDelegate(
    this.children, {
    this.addAutomaticKeepAlives = true,
    this.addRepaintBoundaries = true,
    this.addSemanticIndexes = true,
    this.semanticIndexCallback = _kDefaultSemanticIndexCallback,
    this.semanticIndexOffset = 0,
  }) : _keyToIndex = <Key?, int>{null: 0};
  /// 创建一个使用给定列表为 slivers 提供子组件的委托的常量版本。
  ///
  /// 如果子组件的顺序会改变,考虑使用常规 [SliverChildListDelegate] 构造函数。
  ///
  /// [children]、[addAutomaticKeepAlives]、[addRepaintBoundaries]、
  /// [addSemanticIndexes] 和 [semanticIndexCallback] 参数必须非 null。
  const SliverChildListDelegate.fixed(
    this.children, {
    this.addAutomaticKeepAlives = true,
    this.addRepaintBoundaries = true,
    this.addSemanticIndexes = true,
    this.semanticIndexCallback = _kDefaultSemanticIndexCallback,
    this.semanticIndexOffset = 0,
  }) : _keyToIndex = null;
  /// {@macro flutter.widgets.SliverChildBuilderDelegate.addAutomaticKeepAlives}
  final bool addAutomaticKeepAlives;
  /// {@macro flutter.widgets.SliverChildBuilderDelegate.addRepaintBoundaries}
  final bool addRepaintBoundaries;
  /// {@macro flutter.widgets.SliverChildBuilderDelegate.addSemanticIndexes}
  final bool addSemanticIndexes;
  /// {@macro flutter.widgets.SliverChildBuilderDelegate.semanticIndexOffset}
  final int semanticIndexOffset;
  /// {@macro flutter.widgets.SliverChildBuilderDelegate.semanticIndexCallback}
  final SemanticIndexCallback semanticIndexCallback;
  /// 要显示的组件。
  ///
  /// 如果这个列表将要被改变,通常最好在每个子组件上放一个 [Key],
  /// 这样框架就可以匹配旧配置和新配置,并维护底层的渲染对象。
  ///
  /// 另外,在 Flutter 中,[Widget] 是不可变的,所以直接修改 [children],
  /// 例如 `someWidget.children.add(...)` 或将原始列表值的引用传递给 [children] 参数,
  /// 将导致行为不正确。每次修改子组件列表时,都必须提供一个新的列表对象。
  ///
  /// 下面的代码纠正了上述问题。
  ///
  /// ```dart
  /// class SomeWidgetState extends State<SomeWidget> {
  ///   final List<Widget> _children = <Widget>[];
  ///
  ///   void someHandler() {
  ///     setState(() {
  ///       // 这里的 key 允许 Flutter 重用底层的渲染对象,即使子组件列表被重新创建。
  ///       _children.add(ChildWidget(key: UniqueKey()));
  ///     });
  ///   }
  ///
  ///   @override
  ///   Widget build(BuildContext context) {
  ///     // 由于 Widget 是不可变的,所以总是创建一个新的子组件列表。
  ///     return PageView(children: List<Widget>.of(_children));
  ///   }
  /// }
  /// ```
  final List<Widget> children;
  /// 用于缓存子组件的 key 到 index 查找的映射。
  ///
  /// 在 [_findChildIndex] 的懒加载过程中,_keyToIndex[null] 被用作当前索引。
  /// _keyToIndex 不应该用于查找 null key。
  final Map<Key?, int>? _keyToIndex;
  bool get _isConstantInstance => _keyToIndex == null;
  int? _findChildIndex(Key key) {
    if (_isConstantInstance) {
      return null;
    }
    // 懒加载填充 [_keyToIndex]。
    if (!_keyToIndex!.containsKey(key)) {
      int index = _keyToIndex![null]!;
      while (index < children.length) {
        final Widget child = children[index];
        if (child.key != null) {
          _keyToIndex![child.key] = index;
        }
        if (child.key == key) {
          // 记录当前索引以供下次函数调用。
          _keyToIndex![null] = index + 1;
          return index;
        }
        index += 1;
      }
      _keyToIndex![null] = index;
    } else {
      return _keyToIndex![key];
    }
    return null;
  }
  @override
  int? findIndexByKey(Key key) {
    final Key childKey;
    if (key is _SaltedValueKey) {
      final _SaltedValueKey saltedValueKey = key;
      childKey = saltedValueKey.value;
    } else {
      childKey = key;
    }
    return _findChildIndex(childKey);
  }
  @override
  Widget? build(BuildContext context, int index) {
    if (index < 0 || index >= children.length) {
      return null;
    }
    Widget child = children[index];
    final Key? key = child.key != null? _SaltedValueKey(child.key!) : null;
    if (addRepaintBoundaries) {
      child = RepaintBoundary(child: child);
    }
    if (addSemanticIndexes) {
      final int? semanticIndex = semanticIndexCallback(child, index);
      if (semanticIndex != null) {
        child = IndexedSemantics(index: semanticIndex + semanticIndexOffset, child: child);
      }
    }
    if (addAutomaticKeepAlives) {
      child = AutomaticKeepAlive(child: _SelectionKeepAlive(child: child));
    }
    return KeyedSubtree(key: key, child: child);
  }
  @override
  int? get estimatedChildCount => children.length;
  @override
  bool shouldRebuild(covariant SliverChildListDelegate oldDelegate) {
    return children != oldDelegate.children;
  }
}

F.4 SliverChildBuilderDelegate源码

/// 使用构建器回调为 slivers 提供子组件的委托。
///
/// 许多 slivers 会懒加载它们的 box 子组件,以避免创建比视口可见的子组件更多。
/// 这个委托使用 [NullableIndexedWidgetBuilder] 回调提供子组件,
/// 因此子组件甚至不需要在显示之前就构建。
///
/// 如果 [addAutomaticKeepAlives] 为 true(默认值),则构建器回调返回的组件会自动包装在
/// [AutomaticKeepAlive] 组件中。如果 [addRepaintBoundaries] 为 true(也是默认值),
/// 则会包装在 [RepaintBoundary] 组件中。
///
/// ## 辅助功能
///
/// [CustomScrollView] 要求其语义子组件使用 [IndexedSemantics] 进行注释。
/// 在默认的委托中,当 `addSemanticIndexes` 参数设置为 true 时,会自动完成此操作。
///
/// 如果在单个滚动视图中使用多个委托,则默认情况下索引将不正确。
/// `semanticIndexOffset` 可用于偏移每个委托的语义索引,使索引单调递增。
/// 例如,如果滚动视图包含两个委托,其中第一个有 10 个贡献语义的子组件,
/// 则第二个委托应将其子组件偏移 10。
///
/// {@tool snippet}
///
/// 此示例代码展示了如何使用 `semanticIndexOffset` 处理单个滚动视图中的多个委托。
///
/// ```dart
/// CustomScrollView(
///   semanticChildCount: 4,
///   slivers: <Widget>[
///     SliverGrid(
///       gridDelegate: _gridDelegate,
///       delegate: SliverChildBuilderDelegate(
///         (BuildContext context, int index) {
///            return const Text('...');
///          },
///          childCount: 2,
///        ),
///      ),
///     SliverGrid(
///       gridDelegate: _gridDelegate,
///       delegate: SliverChildBuilderDelegate(
///         (BuildContext context, int index) {
///            return const Text('...');
///          },
///          childCount: 2,
///          semanticIndexOffset: 2,
///        ),
///      ),
///   ],
/// )
/// ```
/// {@end-tool}
///
/// 在某些情况下,只有子组件的子集应使用语义索引进行注释。
/// 例如,在 [ListView.separated()] 中,分隔符没有与之关联的索引。
/// 这是通过提供一个 `semanticIndexCallback` 来完成的,它对分隔符索引返回 null,
/// 并将非分隔符索引减半。
///
/// {@tool snippet}
///
/// 此示例代码展示了如何使用 `semanticIndexCallback` 处理使用语义索引注释子节点的子集。
/// 在奇数索引处有一个 [Spacer] 组件,它不应该有语义索引。
///
/// ```dart
/// CustomScrollView(
///   semanticChildCount: 5,
///   slivers: <Widget>[
///     SliverGrid(
///       gridDelegate: _gridDelegate,
///       delegate: SliverChildBuilderDelegate(
///         (BuildContext context, int index) {
///            if (index.isEven) {
///              return const Text('...');
///            }
///            return const Spacer();
///          },
///          semanticIndexCallback: (Widget widget, int localIndex) {
///            if (localIndex.isEven) {
///              return localIndex ~/ 2;
///            }
///            return null;
///          },
///          childCount: 10,
///        ),
///      ),
///   ],
/// )
/// ```
/// {@end-tool}
///
/// 另请参阅:
///
///  * [SliverChildListDelegate],这是一个具有显式子列表的委托。
///  * [IndexedSemantics],这是手动使用语义索引注释子节点的示例。
class SliverChildBuilderDelegate extends SliverChildDelegate {
  /// 使用给定的构建器回调创建一个为 slivers 提供子组件的委托。
  ///
  /// [builder]、[addAutomaticKeepAlives]、[addRepaintBoundaries]、
  /// [addSemanticIndexes] 和 [semanticIndexCallback] 参数必须非 null。
  ///
  /// 如果 [builder] 返回子组件的顺序有变化,考虑提供一个 [findChildIndexCallback]。
  /// 这允许委托找到之前位于不同索引的子组件的新索引,以便将现有状态附加到新位置的 [Widget] 上。
  const SliverChildBuilderDelegate(
    this.builder, {
    this.findChildIndexCallback,
    this.childCount,
    this.addAutomaticKeepAlives = true,
    this.addRepaintBoundaries = true,
    this.addSemanticIndexes = true,
    this.semanticIndexCallback = _kDefaultSemanticIndexCallback,
    this.semanticIndexOffset = 0,
  });
  /// 用于为 sliver 构建子组件的回调。
  ///
  /// 只会为大于或等于零且小于 [childCount](如果 [childCount] 非空)的索引调用。
  ///
  /// 如果被要求构建一个索引大于存在的索引的组件,应返回 null。
  ///
  /// 如果 [childCount] 为 null 并且 [builder] 总是提供零大小的组件(如 `Container()`
  /// 或 `SizedBox.shrink()`),可能会导致无限循环或内存耗尽。如果可能,提供非零大小的子组件,
  /// 从 [builder] 返回 null,或设置 [childCount]。
  ///
  /// 委托会将此构建器返回的子组件包装在 [RepaintBoundary] 组件中。
  final NullableIndexedWidgetBuilder builder;
  /// 此委托可以提供的子组件总数。
  ///
  /// 如果为 null,子组件的数量由 [builder] 返回 null 的最小索引确定。
  ///
  /// 如果 [childCount] 为 null 并且 [builder] 总是提供零大小的组件(如 `Container()`
  /// 或 `SizedBox.shrink()`),可能会导致无限循环或内存耗尽。如果可能,提供非零大小的子组件,
  /// 从 [builder] 返回 null,或设置 [childCount]。
  final int? childCount;
  /// {@template flutter.widgets.SliverChildBuilderDelegate.addAutomaticKeepAlives}
  /// 是否将每个子组件包装在 [AutomaticKeepAlive] 中。
  ///
  /// 通常,懒加载列表中的子组件会被包装在 [AutomaticKeepAlive] 组件中,
  /// 以便子组件可以使用 [KeepAliveNotification] 在它们否则会被滚动出屏幕时保留其状态。
  ///
  /// 如果子组件将手动维护其 [KeepAlive] 状态,必须禁用此功能(和 [addRepaintBoundaries])。
  /// 如果提前知道没有任何子组件会尝试保持自己活动,禁用此功能可能更有效。
  ///
  /// 默认为 true。
  /// {@endtemplate}
  ///
  /// Defaults to true.
  /// {@endtemplate}
  final bool addAutomaticKeepAlives;
    /// {@template flutter.widgets.SliverChildBuilderDelegate.addRepaintBoundaries}
  /// 是否将每个子组件包装在 [RepaintBoundary] 中。
  ///
  /// 通常,滚动容器中的子组件被包装在重绘边界中,以便在列表滚动时不需要重绘。
  /// 如果子组件易于重绘(例如,纯色块或短文本片段),则不添加重绘边界并始终在滚动期间重绘子组件可能更有效。
  ///
  /// 默认为 true。
  /// {@endtemplate}
  final bool addRepaintBoundaries;
  /// {@template flutter.widgets.SliverChildBuilderDelegate.addSemanticIndexes}
  /// 是否将每个子组件包装在 [IndexedSemantics] 中。
  ///
  /// 通常,滚动容器中的子组件必须使用语义索引进行注释,以生成正确的辅助功能通知。
  /// 只有当索引已由 [IndexedSemantics] 组件提供时,才应将此设置为 false。
  ///
  /// 默认为 true。
  ///
  /// 另请参阅:
  ///
  ///  * [IndexedSemantics],了解如何手动提供语义索引的说明。
  /// {@endtemplate}
  final bool addSemanticIndexes;
  /// {@template flutter.widgets.SliverChildBuilderDelegate.semanticIndexOffset}
  /// 添加到此组件生成的语义索引的初始偏移量。
  ///
  /// 默认为零。
  /// {@endtemplate}
  final int semanticIndexOffset;
  /// {@template flutter.widgets.SliverChildBuilderDelegate.semanticIndexCallback}
  /// 当 [addSemanticIndexes] 为 true 时使用的 [SemanticIndexCallback]。
  ///
  /// 默认为为每个组件提供索引。
  /// {@endtemplate}
  final SemanticIndexCallback semanticIndexCallback;
  /// {@template flutter.widgets.SliverChildBuilderDelegate.findChildIndexCallback}
  /// 在重新排序时,根据其键找到子组件的新索引的回调。
  ///
  /// 如果未提供,当从子组件构建器返回的子组件顺序改变时,
  /// 子组件可能不会映射到其现有的 [RenderObject]。这可能导致状态丢失。
  ///
  /// 此回调应接受一个输入 [Key],并返回与该键关联的子元素的索引,如果未找到则返回 null。
  /// {@endtemplate}
  final ChildIndexGetter? findChildIndexCallback;
  @override
  int? findIndexByKey(Key key) {
    if (findChildIndexCallback == null) {
      return null;
    }
    final Key childKey;
    if (key is _SaltedValueKey) {
      final _SaltedValueKey saltedValueKey = key;
      childKey = saltedValueKey.value;
    } else {
      childKey = key;
    }
    return findChildIndexCallback!(childKey);
  }
  @override
  @pragma('vm:notify-debugger-on-exception')
  Widget? build(BuildContext context, int index) {
    if (index < 0 || (childCount != null && index >= childCount!)) {
      return null;
    }
    Widget? child;
    try {
      child = builder(context, index);
    } catch (exception, stackTrace) {
      child = _createErrorWidget(exception, stackTrace);
    }
    if (child == null) {
      return null;
    }
    final Key? key = child.key != null ? _SaltedValueKey(child.key!) : null;
    if (addRepaintBoundaries) {
      child = RepaintBoundary(child: child);
    }
    if (addSemanticIndexes) {
      final int? semanticIndex = semanticIndexCallback(child, index);
      if (semanticIndex != null) {
        child = IndexedSemantics(index: semanticIndex + semanticIndexOffset, child: child);
      }
    }
    if (addAutomaticKeepAlives) {
      child = AutomaticKeepAlive(child: _SelectionKeepAlive(child: child));
    }
    return KeyedSubtree(key: key, child: child);
  }
  @override
  int? get estimatedChildCount => childCount;
  @override
  bool shouldRebuild(covariant SliverChildBuilderDelegate oldDelegate) => true;
}


目录
相关文章
|
3月前
|
Web App开发 Ubuntu 应用服务中间件
Flutter笔记:Web支持原理与实践
Flutter笔记:Web支持原理与实践
104 0
|
3月前
|
Linux Android开发 iOS开发
Flutter笔记:滑块及其实现分析1
Flutter笔记:滑块及其实现分析1
105 0
|
3月前
|
Web App开发 JSON Android开发
Flutter笔记:获取设备信息
Flutter笔记:获取设备信息
154 0
|
3月前
|
容器
Flutter笔记:Box协议的布局约束原理与应用
Flutter笔记:Box协议的布局约束原理与应用
48 0
|
3月前
|
BI
Flutter笔记:路由观察者
Flutter笔记:路由观察者
75 0
|
3月前
|
存储 编解码 Android开发
Flutter笔记:使用相机
Flutter笔记:使用相机
195 0
|
3月前
|
存储 缓存 Dart
Flutter笔记:目录与文件存储以及在Flutter中的使用(下)
Flutter笔记:目录与文件存储以及在Flutter中的使用(下)
177 0
|
3月前
|
存储 Dart 数据管理
Flutter笔记:目录与文件存储以及在Flutter中的使用(上)
Flutter笔记:目录与文件存储以及在Flutter中的使用(上)
94 0
|
3月前
|
开发者
Flutter笔记:拖拽手势
Flutter笔记:拖拽手势
27 0
|
3月前
|
定位技术
Flutter笔记:缩放手势
Flutter笔记:缩放手势
47 0