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
。
而 SliverChildListDelegate 是 SliverChildDelegate 的一个实现,它 使用一个固定的子组件列表来生成网格的子组件。这里,children
参数就是这个列表。另外,addAutomaticKeepAlives
、addRepaintBoundaries
和 addSemanticIndexes
参数用于控制子组件的生命周期、是否添加重绘边界和语义索引。
另外一方面,该构造函数种调用了其父类(BoxScrollView)的构造函数。semanticChildCount
参数用于语义分析,它表示 GridView 中的子组件数量。如果 semanticChildCount
为 null
,则使用 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, );
从代码可以看到,这个构造函数接收多个参数,其中最重要的两个参数是 gridDelegate
和 itemBuilder
。
gridDelegate
是一个 SliverGridDelegate 对象,它决定了网格的布局。这是一个必需的参数。itemBuilder
是一个函数,它接收一个 BuildContext 和一个索引,然后返回一个 Widget。这个函数只会被调用大于等于零且小于itemCount
的索引。这是一个必需的参数。
GridView.builder 构造函数的工作原理是,当需要渲染一个子部件时,它会调用 itemBuilder
函数,传入当前的 BuildContext 和子部件的索引,然后将返回的 组件 添加到网格中。这样,只有当子部件实际需要显示时,才会调用 itemBuilder
函数创建子部件。
此外,GridView.builder 还接收一些其他参数,如 itemCount
、addAutomaticKeepAlives
、addRepaintBoundaries
和 addSemanticIndexes
,这些参数用于控制 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
实现网格的布局工作:gridDelegate
是 SliverGridDelegate 类型的对象,它是一个委托,负责定义网格的布局。具体来说,它决定了网格中每行的列数,以及每个格子的大小。当 GridView 需要布局其子部件时,它会调用gridDelegate
的方法来获取布局信息。所以,你可以说 gridDelegate 委托了网格的布局工作。childrenDelegate
实现子部件的创建工作:childrenDelegate
是 SliverChildDelegate 类型的对象,它是一个委托,负责创建网格的子部件。具体来说,当 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 构造函数会根据 crossAxisCount
、mainAxisSpacing
、crossAxisSpacing
和 childAspectRatio
的值来布局网格,然后根据 children
列表来创建子部件。这使得你可以轻松地创建一个具有固定列数的网格视图。
在 GridView.count 构造函数中,gridDelegate
被设置为 SliverGridDelegateWithFixedCrossAxisCount 对象。这个对象会根据 crossAxisCount
、mainAxisSpacing
、crossAxisSpacing
和 childAspectRatio
的值来布局网格。
childrenDelegate
被设置为 SliverChildListDelegate 对象,它会根据传入的 children
列表来创建子部件。addAutomaticKeepAlives
、addRepaintBoundaries
和 addSemanticIndexes
参数会传递给 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 构造函数会根据 maxCrossAxisExtent
、mainAxisSpacing
、crossAxisSpacing
和 childAspectRatio
的值来布局网格,然后根据 children
列表来创建子部件。这使得你可以轻松地创建一个具有固定最大宽度的网格视图。
在 GridView.extent 构造函数中,gridDelegate
被设置为 SliverGridDelegateWithMaxCrossAxisExtent 对象。这个对象会根据 maxCrossAxisExtent
、mainAxisSpacing
、crossAxisSpacing
和 childAspectRatio
的值来布局网格。
childrenDelegate
被设置为 SliverChildListDelegate 对象,它会根据传入的 children
列表来创建子部件。addAutomaticKeepAlives
、addRepaintBoundaries
和 addSemanticIndexes
参数会传递给 SliverChildListDelegate,用于控制子部件的生命周期、是否添加重绘边界和语义索引。
2. 什么是委托
2.1 Flutter中委托类的机制
Flutter中,“Delegate” 结尾的类通常表示这些类的主要目的是"委托"(Delegate)某些工作或责任给其他类或者提供了一种扩展或自定义的机制。 —— 这个命名约定的目的是提醒开发者这些类通常用于 委托某种功能 或 提供可插拔性。
具体来说,以下是一些常见情况,“Delegate” 结尾的类可能委托了什么:
- 业务逻辑委托:某些 “Delegate” 类可能用于将特定的业务逻辑委托给其他类。例如,一个
ListView.builder
中的itemBuilder
参数允许你委托生成列表项的逻辑给开发者自定义的函数。 - 路由过渡委托:一些类用于定义页面路由之间的过渡效果,比如
PageRouteBuilder
,允许开发者委托定义过渡效果的工作。 - 渲染和布局委托:在渲染和布局阶段,一些 “Delegate” 类允许你自定义渲染对象的外观和布局,除了这里介绍的 GridView使用两类 Delegate 分别实现提供子组件和子组件布局外——再比如
SliverPersistentHeaderDelegate
用于定义 SliverAppBar 的外观和行为。 - 插件或扩展机制:一些 “Delegate” 类可能用于实现插件或扩展的接口,这允许开发者自定义、扩展或替换某些功能的实现。
- 策略模式的委托:某些 “Delegate” 类实际上是策略模式的实现,允许你根据需要在不同的算法或行为之间切换,而不需要修改客户端代码。
“Delegate” 结尾的类通常表示它们起到了一种"桥梁"或"中介"的作用,将某些具体的实现委托给其他类,以实现灵活性、可定制性和可扩展性。这种命名约定有助于在代码中识别和理解这些类的用途和作用。
2.1 设计原则与模式
Flutter中以 “Delegate” 结尾的这些类通常,以分离不同部分之间的关注点并提供更好的可维护性和可扩展性。这些 “Delegate” 类通常遵循某些规则和设计原则,如以下所示:
- 分离关注点:“Delegate” 类有助于将代码拆分为不同的部分,每个部分关注特定的责任。这有助于减少代码的复杂性,提高可读性和可维护性。
- 单一责任原则(SRP):“Delegate” 类通常遵循 SRP,每个类关注一个单一的责任。这使得代码更容易测试、理解和维护。
- 策略模式:在许多情况下, “Delegate” 类实现策略模式,允许您根据需要在不同的算法或行为之间切换。这种设计模式有助于动态地选择不同的实现,而不需要修改客户端代码。
- 插件机制:许多 Delegate 类用于实现插件或扩展的接口,以便轻松添加和扩展功能。这样,开发人员可以创建自定义实现以满足其需求,而不必修改核心代码。
这些 Delegate 类的共同特点是它们提供了灵活性、可扩展性和可定制性,使得 Flutter· 应用程序可以更容易地满足不同的需求。通过使用这些类,开发人员可以更轻松地实现一些通用的设计原则,例如单一责任原则和策略模式,以改进应用程序的结构和性能。
3. 再看 GridView
3.1 回顾
本文 第1小节 介绍了 GridView 的用法。 GridView 有多种构造函数,其中一些构造函数使用了委托(Delegate)的概念,例如 GridView.builder 和 GridView.custom,现在以这两个构造函数简单回顾一下。
- GridView.builder:这个构造函数允许你动态地创建网格项目。它需要一个 GridDelegate 和一个 IndexedWidgetBuilder。GridDelegate 用于控制网格的布局,IndexedWidgetBuilder 用于生成网格项目。其中:
- GridDelegate是一个抽象类,它有多个实现,例如:SliverGridDelegateWithFixedCrossAxisCount 和 SliverGridDelegateWithMaxCrossAxisExtent。这些实现类允许你自定义网格的布局,例如设置交叉轴的项目数量或最大尺寸。
- IndexedWidgetBuilder 是一个回调函数,它接收一个上下文和一个索引,返回一个新的组件。你可以在这个回调函数中根据索引生成不同的组件。
- GridView.
custom
:这个构造函数允许你使用自定义的SliverChildDelegate来生成网格项目。其中:
- SliverChildDelegate 是一个抽象类,它有多个实现,例如SliverChildBuilderDelegate 和 SliverChildListDelegate。这些实现类允许你自定义网格项目的生成方式,例如使用一个回调函数或一个固定的小组件列表。
在这两个构造函数中,GridView 组件都使用了 Delegate 的类来提供灵活性和可扩展性。
通过使用这些类,实现定义网格的布局和项目的生成方式,而不需要修改 GridView 组件的源代码。
这应该算是一种的 策略模式 的应用,它 允许你在不同的策略之间动态地切换,以满足不同的需求。
进一步看 GridView 的代码,还需要在先介绍 GridView 的 gridDelegate 属性 和 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) 阶段,为了在子元素滚动进出视图时 保留状态,有以下可能的选项:
- 将非琐碎的驱动 UI 状态的业务逻辑的所有权移出
sliver
子组件子树; - 让 KeepAlive 成为需要保留的
sliver
子组件子树的根组件; - 使用 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,它在交叉轴上具有固定数量的单元格。
=> 这意味着无论网格的总体大小如何,交叉轴上的单元格数量始终保持不变。这对于需要创建均匀分布的网格布局(例如图片网格)非常有用。此外,你还可以控制单元格之间的间距(mainAxisSpacing
和 crossAxisSpacing
)、单元格的宽高比(childAspectRatio
)以及网格的内边距(padding
)。
(2) SliverGridDelegateWithMaxCrossAxisExtent extends SliverGridDelegate
SliverGridDelegateWithMaxCrossAxisExtent 是一个用于为 slivers
提供网格布局的委托Delegate,它在交叉轴上的单元格具有最大扩展尺寸。
=> 这意味着单元格的尺寸会自动调整以适应网格的总体大小,但不会超过设定的最大尺寸。这对于需要创建响应式布局的网格非常有用,因为你可以根据屏幕的大小自动调整单元格的数量。此外,你还可以控制单元格之间的间距(mainAxisSpacing
和 crossAxisSpacing
)、单元格的宽高比(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 是一个抽象类,它有两个常用的子类:SliverGridDelegateWithFixedCrossAxisCount 和 SliverGridDelegateWithMaxCrossAxisExtent。
- SliverGridDelegateWithFixedCrossAxisCount 创建一个网格,其中交叉轴上有固定数量的格子。你可以指定交叉轴上的格子数量,以及格子之间的间距和宽高比。
- SliverGridDelegateWithMaxCrossAxisExtent 创建一个网格,其中交叉轴上的每个格子都有最大的宽度。你可以指定每个格子的最大宽度,以及格子之间的间距和宽高比。
在 GridView、GridView.builder 和 GridView.custom 构造函数中,你可以明确指定 gridDelegate
。在其他构造函数中,gridDelegate
会自动创建。
3.4 GridView 的 childrenDelegate属性
/// 一个委托,为 [GridView] 提供子部件。 /// /// [GridView.custom] 构造函数允许你明确指定这个委托。其他构造函数创建一个包装给定子部件列表的 [childrenDelegate]。 final SliverChildDelegate childrenDelegate;
可以看到,childrenDelegate
属性类型为 SliverChildDelegate。这个属性是一个 委托(delegate),它决定了如何为 GridView 创建子部件。
SliverChildDelegate 是一个抽象类,它有两个常用的子类:SliverChildListDelegate 和 SliverChildBuilderDelegate。其中:
- 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实现的,其中需要指定两个信息:
- 如何创建和布局子项:delegate参数;
SliverGrid 的delegate
参数类型为 SliverChildDelegate,它负责提供和构建子部件。childrenDelegate 是传递给 delegate 的参数,它可能是 SliverChildListDelegate(用于处理固定数量的子部件)或 SliverChildBuilderDelegate(用于处理懒加载的子部件)。 - 网格的布局:gridDelegate参数。
3.6 这就是Flutter设计中的 委托
在GridView的 buildChildLayout
方法中,我们可以看到委托(Delegate)的强大之处。
GridView 本身并没有实现如何创建和布局子项,以及如何布局网格,而是将这些工作委托给了 childrenDelegate 和 gridDelegate。
childrenDelegate
是一个 SliverChildDelegate 对象,它决定了如何创建和布局子项。这个委托可以是 SliverChildListDelegate(用于处理固定数量的子部件)或 SliverChildBuilderDelegate(用于处理懒加载的子部件)。通过这个委托,GridView 可以灵活地创建和布局子项,无论是固定数量的子部件,还是懒加载的子部件。
gridDelegate
是一个 SliverGridDelegate 对象,它决定了网格的布局。这个委托可以是 SliverGridDelegateWithFixedCrossAxisCount(固定列数的网格)或 SliverGridDelegateWithMaxCrossAxisExtent(最大交叉轴范围的网格)。通过这个委托,GridView 可以灵活地布局网格,无论是固定列数的网格,还是最大交叉轴范围的网格。
总的来说,通过委托(Delegate)的方式,GridView 将创建和布局子项,以及布局网格的工作交给了其他对象,从而实现了代码的解耦和复用。
4. 小结与启示
在 Flutter 中,委托(Delegate)类 可以认为是一种 设计模式,它允许一个类将某些任务或责任委托给另一个类。这种设计模式可以提高代码的 灵活性 和 可复用性,因为它允许我们在不修改原有类的情况下,改变或扩展某些功能。
在 GridView 的例子中,我们看到了两种类型的委托:SliverChildDelegate 和 SliverGridDelegate。SliverChildDelegate 负责创建和布局子项,而 SliverGridDelegate 负责布局网格。通过这两个委托,GridView 可以灵活地创建和布局子项,以及布局网格,而无需自己实现这些功能。
这种设计模式的优点是:
- 分离关注点:委托允许我们将不同的功能分离到不同的类中,每个类只关注一个特定的任务或责任。这有助于减少代码的复杂性,提高可读性和可维护性。
- 提高复用性:通过委托,我们可以在不同的上下文中复用同一个类。例如,我们可以在不同的 GridView 中复用同一个 SliverChildDelegate 或 SliverGridDelegate。
- 提高灵活性:通过委托,我们可以在运行时改变某些功能的实现。例如,我们可以在运行时改变 GridView 的 gridDelegate,从而改变网格的布局。
这种模式可以应用于以后的编程中。例如,假设我们正在设计一个自定义的列表组件 MyListView,我们可以设计两个委托:ItemBuilder 和 ItemLayoutDelegate。ItemBuilder 负责创建列表项,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; }