1// Copyright 2014 The Flutter Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5/// @docImport 'scroll_view.dart';
6/// @docImport 'sliver.dart';
7/// @docImport 'spacer.dart';
8/// @docImport 'two_dimensional_scroll_view.dart';
9/// @docImport 'viewport.dart';
10library;
11
12import 'package:flutter/foundation.dart';
13import 'package:flutter/rendering.dart';
14
15import 'automatic_keep_alive.dart';
16import 'basic.dart';
17import 'framework.dart';
18import 'selection_container.dart';
19import 'two_dimensional_viewport.dart';
20
21export 'package:flutter/rendering.dart'
22 show
23 SliverGridDelegate,
24 SliverGridDelegateWithFixedCrossAxisCount,
25 SliverGridDelegateWithMaxCrossAxisExtent;
26
27// Examples can assume:
28// late SliverGridDelegateWithMaxCrossAxisExtent _gridDelegate;
29// abstract class SomeWidget extends StatefulWidget { const SomeWidget({super.key}); }
30// typedef ChildWidget = Placeholder;
31
32/// A callback which produces a semantic index given a widget and the local index.
33///
34/// Return a null value to prevent a widget from receiving an index.
35///
36/// A semantic index is used to tag child semantic nodes for accessibility
37/// announcements in scroll view.
38///
39/// See also:
40///
41/// * [CustomScrollView], for an explanation of scroll semantics.
42/// * [SliverChildBuilderDelegate], for an explanation of how this is used to
43/// generate indexes.
44typedef SemanticIndexCallback = int? Function(Widget widget, int localIndex);
45
46int _kDefaultSemanticIndexCallback(Widget _, int localIndex) => localIndex;
47
48/// A delegate that supplies children for slivers.
49///
50/// Many slivers lazily construct their box children to avoid creating more
51/// children than are visible through the [Viewport]. Rather than receiving
52/// their children as an explicit [List], they receive their children using a
53/// [SliverChildDelegate].
54///
55/// It's uncommon to subclass [SliverChildDelegate]. Instead, consider using one
56/// of the existing subclasses that provide adaptors to builder callbacks or
57/// explicit child lists.
58///
59/// {@template flutter.widgets.SliverChildDelegate.lifecycle}
60/// ## Child elements' lifecycle
61///
62/// ### Creation
63///
64/// While laying out the list, visible children's elements, states and render
65/// objects will be created lazily based on existing widgets (such as in the
66/// case of [SliverChildListDelegate]) or lazily provided ones (such as in the
67/// case of [SliverChildBuilderDelegate]).
68///
69/// ### Destruction
70///
71/// When a child is scrolled out of view, the associated element subtree, states
72/// and render objects are destroyed. A new child at the same position in the
73/// sliver will be lazily recreated along with new elements, states and render
74/// objects when it is scrolled back.
75///
76/// ### Destruction mitigation
77///
78/// In order to preserve state as child elements are scrolled in and out of
79/// view, the following options are possible:
80///
81/// * Moving the ownership of non-trivial UI-state-driving business logic
82/// out of the sliver child subtree. For instance, if a list contains posts
83/// with their number of upvotes coming from a cached network response, store
84/// the list of posts and upvote number in a data model outside the list. Let
85/// the sliver child UI subtree be easily recreate-able from the
86/// source-of-truth model object. Use [StatefulWidget]s in the child widget
87/// subtree to store instantaneous UI state only.
88///
89/// * Letting [KeepAlive] be the root widget of the sliver child widget subtree
90/// that needs to be preserved. The [KeepAlive] widget marks the child
91/// subtree's top render object child for keepalive. When the associated top
92/// render object is scrolled out of view, the sliver keeps the child's
93/// render object (and by extension, its associated elements and states) in a
94/// cache list instead of destroying them. When scrolled back into view, the
95/// render object is repainted as-is (if it wasn't marked dirty in the
96/// interim).
97///
98/// This only works if the [SliverChildDelegate] subclasses don't wrap the
99/// child widget subtree with other widgets such as [AutomaticKeepAlive] and
100/// [RepaintBoundary] via `addAutomaticKeepAlives` and
101/// `addRepaintBoundaries`.
102///
103/// * Using [AutomaticKeepAlive] widgets (inserted by default in
104/// [SliverChildListDelegate] or [SliverChildListDelegate]).
105/// [AutomaticKeepAlive] allows descendant widgets to control whether the
106/// subtree is actually kept alive or not. This behavior is in contrast with
107/// [KeepAlive], which will unconditionally keep the subtree alive.
108///
109/// As an example, the [EditableText] widget signals its sliver child element
110/// subtree to stay alive while its text field has input focus. If it doesn't
111/// have focus and no other descendants signaled for keepalive via a
112/// [KeepAliveNotification], the sliver child element subtree will be
113/// destroyed when scrolled away.
114///
115/// [AutomaticKeepAlive] descendants typically signal it to be kept alive by
116/// using the [AutomaticKeepAliveClientMixin], then implementing the
117/// [AutomaticKeepAliveClientMixin.wantKeepAlive] getter and calling
118/// [AutomaticKeepAliveClientMixin.updateKeepAlive].
119///
120/// ## Using more than one delegate in a [Viewport]
121///
122/// If multiple delegates are used in a single scroll view, the first child of
123/// each delegate will always be laid out, even if it extends beyond the
124/// currently viewable area. This is because at least one child is required in
125/// order to [estimateMaxScrollOffset] for the whole scroll view, as it uses the
126/// currently built children to estimate the remaining children's extent.
127/// {@endtemplate}
128///
129/// See also:
130///
131/// * [SliverChildBuilderDelegate], which is a delegate that uses a builder
132/// callback to construct the children.
133/// * [SliverChildListDelegate], which is a delegate that has an explicit list
134/// of children.
135abstract class SliverChildDelegate {
136 /// Abstract const constructor. This constructor enables subclasses to provide
137 /// const constructors so that they can be used in const expressions.
138 const SliverChildDelegate();
139
140 /// Returns the child with the given index.
141 ///
142 /// Should return null if asked to build a widget with a greater
143 /// index than exists. If this returns null, [estimatedChildCount]
144 /// must subsequently return a precise non-null value (which is then
145 /// used to implement [RenderSliverBoxChildManager.childCount]).
146 ///
147 /// Subclasses typically override this function and wrap their children in
148 /// [AutomaticKeepAlive], [IndexedSemantics], and [RepaintBoundary] widgets.
149 ///
150 /// The values returned by this method are cached. To indicate that the
151 /// widgets have changed, a new delegate must be provided, and the new
152 /// delegate's [shouldRebuild] method must return true.
153 Widget? build(BuildContext context, int index);
154
155 /// Returns an estimate of the number of children this delegate will build.
156 ///
157 /// Used to estimate the maximum scroll offset if [estimateMaxScrollOffset]
158 /// returns null.
159 ///
160 /// Return null if there are an unbounded number of children or if it would
161 /// be too difficult to estimate the number of children.
162 ///
163 /// This must return a precise number once [build] has returned null, as it
164 /// used to implement [RenderSliverBoxChildManager.childCount].
165 int? get estimatedChildCount => null;
166
167 /// Returns an estimate of the max scroll extent for all the children.
168 ///
169 /// Subclasses should override this function if they have additional
170 /// information about their max scroll extent.
171 ///
172 /// The default implementation returns null, which causes the caller to
173 /// extrapolate the max scroll offset from the given parameters.
174 double? estimateMaxScrollOffset(
175 int firstIndex,
176 int lastIndex,
177 double leadingScrollOffset,
178 double trailingScrollOffset,
179 ) => null;
180
181 /// Called at the end of layout to indicate that layout is now complete.
182 ///
183 /// The `firstIndex` argument is the index of the first child that was
184 /// included in the current layout. The `lastIndex` argument is the index of
185 /// the last child that was included in the current layout.
186 ///
187 /// Useful for subclasses that which to track which children are included in
188 /// the underlying render tree.
189 void didFinishLayout(int firstIndex, int lastIndex) {}
190
191 /// Called whenever a new instance of the child delegate class is
192 /// provided to the sliver.
193 ///
194 /// If the new instance represents different information than the old
195 /// instance, then the method should return true, otherwise it should return
196 /// false.
197 ///
198 /// If the method returns false, then the [build] call might be optimized
199 /// away.
200 bool shouldRebuild(covariant SliverChildDelegate oldDelegate);
201
202 /// Find index of child element with associated key.
203 ///
204 /// This will be called during `performRebuild` in [SliverMultiBoxAdaptorElement]
205 /// to check if a child has moved to a different position. It should return the
206 /// index of the child element with associated key, null if not found.
207 ///
208 /// If not provided, a child widget may not map to its existing [RenderObject]
209 /// when the order of children returned from the children builder changes.
210 /// This may result in state-loss.
211 int? findIndexByKey(Key key) => null;
212
213 @override
214 String toString() {
215 final List<String> description = <String>[];
216 debugFillDescription(description);
217 return '${describeIdentity(this)}(${description.join(", ")})';
218 }
219
220 /// Add additional information to the given description for use by [toString].
221 @protected
222 @mustCallSuper
223 void debugFillDescription(List<String> description) {
224 try {
225 final int? children = estimatedChildCount;
226 if (children != null) {
227 description.add('estimated child count: $children');
228 }
229 } catch (e) {
230 // The exception is forwarded to widget inspector.
231 description.add('estimated child count: EXCEPTION (${e.runtimeType})');
232 }
233 }
234}
235
236class _SaltedValueKey extends ValueKey<Key> {
237 const _SaltedValueKey(super.value);
238}
239
240/// Called to find the new index of a child based on its `key` in case of
241/// reordering.
242///
243/// If the child with the `key` is no longer present, null is returned.
244///
245/// Used by [SliverChildBuilderDelegate.findChildIndexCallback].
246typedef ChildIndexGetter = int? Function(Key key);
247
248/// A delegate that supplies children for slivers using a builder callback.
249///
250/// Many slivers lazily construct their box children to avoid creating more
251/// children than are visible through the [Viewport]. This delegate provides
252/// children using a [NullableIndexedWidgetBuilder] callback, so that the children do
253/// not even have to be built until they are displayed.
254///
255/// The widgets returned from the builder callback are automatically wrapped in
256/// [AutomaticKeepAlive] widgets if [addAutomaticKeepAlives] is true (the
257/// default) and in [RepaintBoundary] widgets if [addRepaintBoundaries] is true
258/// (also the default).
259///
260/// ## Accessibility
261///
262/// The [CustomScrollView] requires that its semantic children are annotated
263/// using [IndexedSemantics]. This is done by default in the delegate with
264/// the `addSemanticIndexes` parameter set to true.
265///
266/// If multiple delegates are used in a single scroll view, then the indexes
267/// will not be correct by default. The `semanticIndexOffset` can be used to
268/// offset the semantic indexes of each delegate so that the indexes are
269/// monotonically increasing. For example, if a scroll view contains two
270/// delegates where the first has 10 children contributing semantics, then the
271/// second delegate should offset its children by 10.
272///
273/// {@tool snippet}
274///
275/// This sample code shows how to use `semanticIndexOffset` to handle multiple
276/// delegates in a single scroll view.
277///
278/// ```dart
279/// CustomScrollView(
280/// semanticChildCount: 4,
281/// slivers: <Widget>[
282/// SliverGrid(
283/// gridDelegate: _gridDelegate,
284/// delegate: SliverChildBuilderDelegate(
285/// (BuildContext context, int index) {
286/// return const Text('...');
287/// },
288/// childCount: 2,
289/// ),
290/// ),
291/// SliverGrid(
292/// gridDelegate: _gridDelegate,
293/// delegate: SliverChildBuilderDelegate(
294/// (BuildContext context, int index) {
295/// return const Text('...');
296/// },
297/// childCount: 2,
298/// semanticIndexOffset: 2,
299/// ),
300/// ),
301/// ],
302/// )
303/// ```
304/// {@end-tool}
305///
306/// In certain cases, only a subset of child widgets should be annotated
307/// with a semantic index. For example, in [ListView.separated()] the
308/// separators do not have an index associated with them. This is done by
309/// providing a `semanticIndexCallback` which returns null for separators
310/// indexes and rounds the non-separator indexes down by half.
311///
312/// {@tool snippet}
313///
314/// This sample code shows how to use `semanticIndexCallback` to handle
315/// annotating a subset of child nodes with a semantic index. There is
316/// a [Spacer] widget at odd indexes which should not have a semantic
317/// index.
318///
319/// ```dart
320/// CustomScrollView(
321/// semanticChildCount: 5,
322/// slivers: <Widget>[
323/// SliverGrid(
324/// gridDelegate: _gridDelegate,
325/// delegate: SliverChildBuilderDelegate(
326/// (BuildContext context, int index) {
327/// if (index.isEven) {
328/// return const Text('...');
329/// }
330/// return const Spacer();
331/// },
332/// semanticIndexCallback: (Widget widget, int localIndex) {
333/// if (localIndex.isEven) {
334/// return localIndex ~/ 2;
335/// }
336/// return null;
337/// },
338/// childCount: 10,
339/// ),
340/// ),
341/// ],
342/// )
343/// ```
344/// {@end-tool}
345///
346/// See also:
347///
348/// * [SliverChildListDelegate], which is a delegate that has an explicit list
349/// of children.
350/// * [IndexedSemantics], for an example of manually annotating child nodes
351/// with semantic indexes.
352class SliverChildBuilderDelegate extends SliverChildDelegate {
353 /// Creates a delegate that supplies children for slivers using the given
354 /// builder callback.
355 ///
356 /// The [builder], [addAutomaticKeepAlives], [addRepaintBoundaries],
357 /// [addSemanticIndexes], and [semanticIndexCallback] arguments must not be
358 /// null.
359 ///
360 /// If the order in which [builder] returns children ever changes, consider
361 /// providing a [findChildIndexCallback]. This allows the delegate to find the
362 /// new index for a child that was previously located at a different index to
363 /// attach the existing state to the [Widget] at its new location.
364 const SliverChildBuilderDelegate(
365 this.builder, {
366 this.findChildIndexCallback,
367 this.childCount,
368 this.addAutomaticKeepAlives = true,
369 this.addRepaintBoundaries = true,
370 this.addSemanticIndexes = true,
371 this.semanticIndexCallback = _kDefaultSemanticIndexCallback,
372 this.semanticIndexOffset = 0,
373 });
374
375 /// Called to build children for the sliver.
376 ///
377 /// Will be called only for indices greater than or equal to zero and less
378 /// than [childCount] (if [childCount] is non-null).
379 ///
380 /// Should return null if asked to build a widget with a greater index than
381 /// exists.
382 ///
383 /// May result in an infinite loop or run out of memory if [childCount] is null
384 /// and the [builder] always provides a zero-size widget (such as `Container()`
385 /// or `SizedBox.shrink()`). If possible, provide children with non-zero size,
386 /// return null from [builder], or set a [childCount].
387 ///
388 /// The delegate wraps the children returned by this builder in
389 /// [RepaintBoundary] widgets.
390 final NullableIndexedWidgetBuilder builder;
391
392 /// The total number of children this delegate can provide.
393 ///
394 /// If null, the number of children is determined by the least index for which
395 /// [builder] returns null.
396 ///
397 /// May result in an infinite loop or run out of memory if [childCount] is null
398 /// and the [builder] always provides a zero-size widget (such as `Container()`
399 /// or `SizedBox.shrink()`). If possible, provide children with non-zero size,
400 /// return null from [builder], or set a [childCount].
401 final int? childCount;
402
403 /// {@template flutter.widgets.SliverChildBuilderDelegate.addAutomaticKeepAlives}
404 /// Whether to wrap each child in an [AutomaticKeepAlive].
405 ///
406 /// Typically, lazily laid out children are wrapped in [AutomaticKeepAlive]
407 /// widgets so that the children can use [KeepAliveNotification]s to preserve
408 /// their state when they would otherwise be garbage collected off-screen.
409 ///
410 /// This feature (and [addRepaintBoundaries]) must be disabled if the children
411 /// are going to manually maintain their [KeepAlive] state. It may also be
412 /// more efficient to disable this feature if it is known ahead of time that
413 /// none of the children will ever try to keep themselves alive.
414 ///
415 /// Defaults to true.
416 ///
417 /// {@tool dartpad}
418 /// This sample demonstrates how to use the [AutomaticKeepAlive] widget in
419 /// combination with the [AutomaticKeepAliveClientMixin] to selectively preserve
420 /// the state of individual items in a scrollable list.
421 ///
422 /// Normally, widgets in a lazily built list like [ListView.builder] are
423 /// disposed of when they leave the visible area to maintain performance. This means
424 /// that any state inside a [StatefulWidget] would be lost unless explicitly
425 /// preserved.
426 ///
427 /// In this example, each list item is a [StatefulWidget] that includes a
428 /// counter and an increment button. To preserve the state of selected items
429 /// (based on their index), the [AutomaticKeepAlive] widget and
430 /// [AutomaticKeepAliveClientMixin] are used:
431 ///
432 /// - The `wantKeepAlive` getter in the item’s state class returns true for
433 /// even-indexed items, indicating that their state should be preserved.
434 /// - For odd-indexed items, `wantKeepAlive` returns false, so their state is
435 /// not preserved when scrolled out of view.
436 ///
437 /// ** See code in examples/api/lib/widgets/keep_alive/automatic_keep_alive.0.dart **
438 /// {@end-tool}
439 ///
440 /// {@tool dartpad}
441 /// This sample demonstrates how to use the [KeepAlive] widget
442 /// to preserve the state of individual list items in a [ListView] when they are
443 /// scrolled out of view.
444 ///
445 /// By default, [ListView.builder] only keeps the widgets currently visible in
446 /// the viewport alive. When an item scrolls out of view, it may be disposed to
447 /// free up resources. This can cause the state of [StatefulWidget]s to be lost
448 /// if not explicitly preserved.
449 ///
450 /// In this example, each item in the list is a [StatefulWidget] that maintains
451 /// a counter. Tapping the "+" button increments the counter. To selectively
452 /// preserve the state, each item is wrapped in a [KeepAlive] widget, with the
453 /// keepAlive parameter set based on the item’s index:
454 ///
455 /// - For even-indexed items, `keepAlive: true`, so their state is preserved
456 /// even when scrolled off-screen.
457 /// - For odd-indexed items, `keepAlive: false`, so their state is discarded
458 /// when they are no longer visible.
459 ///
460 /// ** See code in examples/api/lib/widgets/keep_alive/keep_alive.0.dart **
461 /// {@end-tool}
462 ///
463 /// * [AutomaticKeepAlive], which allows subtrees to request to be kept alive
464 /// in lazy lists.
465 /// * [AutomaticKeepAliveClientMixin], which is a mixin with convenience
466 /// methods for clients of [AutomaticKeepAlive]. Used with [State]
467 /// subclasses.
468 /// * [KeepAlive] which marks a child as needing to stay alive even when it's
469 /// in a lazy list that would otherwise remove it.
470 /// {@endtemplate}
471 final bool addAutomaticKeepAlives;
472
473 /// {@template flutter.widgets.SliverChildBuilderDelegate.addRepaintBoundaries}
474 /// Whether to wrap each child in a [RepaintBoundary].
475 ///
476 /// Typically, children in a scrolling container are wrapped in repaint
477 /// boundaries so that they do not need to be repainted as the list scrolls.
478 /// If the children are easy to repaint (e.g., solid color blocks or a short
479 /// snippet of text), it might be more efficient to not add a repaint boundary
480 /// and instead always repaint the children during scrolling.
481 ///
482 /// Defaults to true.
483 /// {@endtemplate}
484 final bool addRepaintBoundaries;
485
486 /// {@template flutter.widgets.SliverChildBuilderDelegate.addSemanticIndexes}
487 /// Whether to wrap each child in an [IndexedSemantics].
488 ///
489 /// Typically, children in a scrolling container must be annotated with a
490 /// semantic index in order to generate the correct accessibility
491 /// announcements. This should only be set to false if the indexes have
492 /// already been provided by an [IndexedSemantics] widget.
493 ///
494 /// Defaults to true.
495 ///
496 /// See also:
497 ///
498 /// * [IndexedSemantics], for an explanation of how to manually
499 /// provide semantic indexes.
500 /// {@endtemplate}
501 final bool addSemanticIndexes;
502
503 /// {@template flutter.widgets.SliverChildBuilderDelegate.semanticIndexOffset}
504 /// An initial offset to add to the semantic indexes generated by this widget.
505 ///
506 /// Defaults to zero.
507 /// {@endtemplate}
508 final int semanticIndexOffset;
509
510 /// {@template flutter.widgets.SliverChildBuilderDelegate.semanticIndexCallback}
511 /// A [SemanticIndexCallback] which is used when [addSemanticIndexes] is true.
512 ///
513 /// Defaults to providing an index for each widget.
514 /// {@endtemplate}
515 final SemanticIndexCallback semanticIndexCallback;
516
517 /// {@template flutter.widgets.SliverChildBuilderDelegate.findChildIndexCallback}
518 /// Called to find the new index of a child based on its key in case of reordering.
519 ///
520 /// If not provided, a child widget may not map to its existing [RenderObject]
521 /// when the order of children returned from the children builder changes.
522 /// This may result in state-loss.
523 ///
524 /// This callback should take an input [Key], and it should return the
525 /// index of the child element with that associated key, or null if not found.
526 /// {@endtemplate}
527 final ChildIndexGetter? findChildIndexCallback;
528
529 @override
530 int? findIndexByKey(Key key) {
531 if (findChildIndexCallback == null) {
532 return null;
533 }
534 final Key childKey;
535 if (key is _SaltedValueKey) {
536 final _SaltedValueKey saltedValueKey = key;
537 childKey = saltedValueKey.value;
538 } else {
539 childKey = key;
540 }
541 return findChildIndexCallback!(childKey);
542 }
543
544 @override
545 @pragma('vm:notify-debugger-on-exception')
546 Widget? build(BuildContext context, int index) {
547 if (index < 0 || (childCount != null && index >= childCount!)) {
548 return null;
549 }
550 Widget? child;
551 try {
552 child = builder(context, index);
553 } catch (exception, stackTrace) {
554 child = _createErrorWidget(exception, stackTrace);
555 }
556 if (child == null) {
557 return null;
558 }
559 final Key? key = child.key != null ? _SaltedValueKey(child.key!) : null;
560 if (addRepaintBoundaries) {
561 child = RepaintBoundary(child: child);
562 }
563 if (addSemanticIndexes) {
564 final int? semanticIndex = semanticIndexCallback(child, index);
565 if (semanticIndex != null) {
566 child = IndexedSemantics(index: semanticIndex + semanticIndexOffset, child: child);
567 }
568 }
569 if (addAutomaticKeepAlives) {
570 child = AutomaticKeepAlive(child: _SelectionKeepAlive(child: child));
571 }
572 return KeyedSubtree(key: key, child: child);
573 }
574
575 @override
576 int? get estimatedChildCount => childCount;
577
578 @override
579 bool shouldRebuild(covariant SliverChildBuilderDelegate oldDelegate) => true;
580}
581
582/// A delegate that supplies children for slivers using an explicit list.
583///
584/// Many slivers lazily construct their box children to avoid creating more
585/// children than are visible through the [Viewport]. This delegate provides
586/// children using an explicit list, which is convenient but reduces the benefit
587/// of building children lazily.
588///
589/// In general building all the widgets in advance is not efficient. It is
590/// better to create a delegate that builds them on demand using
591/// [SliverChildBuilderDelegate] or by subclassing [SliverChildDelegate]
592/// directly.
593///
594/// This class is provided for the cases where either the list of children is
595/// known well in advance (ideally the children are themselves compile-time
596/// constants, for example), and therefore will not be built each time the
597/// delegate itself is created, or the list is small, such that it's likely
598/// always visible (and thus there is nothing to be gained by building it on
599/// demand). For example, the body of a dialog box might fit both of these
600/// conditions.
601///
602/// The widgets in the given [children] list are automatically wrapped in
603/// [AutomaticKeepAlive] widgets if [addAutomaticKeepAlives] is true (the
604/// default) and in [RepaintBoundary] widgets if [addRepaintBoundaries] is true
605/// (also the default).
606///
607/// ## Accessibility
608///
609/// The [CustomScrollView] requires that its semantic children are annotated
610/// using [IndexedSemantics]. This is done by default in the delegate with
611/// the `addSemanticIndexes` parameter set to true.
612///
613/// If multiple delegates are used in a single scroll view, then the indexes
614/// will not be correct by default. The `semanticIndexOffset` can be used to
615/// offset the semantic indexes of each delegate so that the indexes are
616/// monotonically increasing. For example, if a scroll view contains two
617/// delegates where the first has 10 children contributing semantics, then the
618/// second delegate should offset its children by 10.
619///
620/// In certain cases, only a subset of child widgets should be annotated
621/// with a semantic index. For example, in [ListView.separated()] the
622/// separators do not have an index associated with them. This is done by
623/// providing a `semanticIndexCallback` which returns null for separators
624/// indexes and rounds the non-separator indexes down by half.
625///
626/// See [SliverChildBuilderDelegate] for sample code using
627/// `semanticIndexOffset` and `semanticIndexCallback`.
628///
629/// See also:
630///
631/// * [SliverChildBuilderDelegate], which is a delegate that uses a builder
632/// callback to construct the children.
633class SliverChildListDelegate extends SliverChildDelegate {
634 /// Creates a delegate that supplies children for slivers using the given
635 /// list.
636 ///
637 /// The [children], [addAutomaticKeepAlives], [addRepaintBoundaries],
638 /// [addSemanticIndexes], and [semanticIndexCallback] arguments must not be
639 /// null.
640 ///
641 /// If the order of children never changes, consider using the constant
642 /// [SliverChildListDelegate.fixed] constructor.
643 SliverChildListDelegate(
644 this.children, {
645 this.addAutomaticKeepAlives = true,
646 this.addRepaintBoundaries = true,
647 this.addSemanticIndexes = true,
648 this.semanticIndexCallback = _kDefaultSemanticIndexCallback,
649 this.semanticIndexOffset = 0,
650 }) : _keyToIndex = <Key?, int>{null: 0};
651
652 /// Creates a constant version of the delegate that supplies children for
653 /// slivers using the given list.
654 ///
655 /// If the order of the children will change, consider using the regular
656 /// [SliverChildListDelegate] constructor.
657 ///
658 /// The [children], [addAutomaticKeepAlives], [addRepaintBoundaries],
659 /// [addSemanticIndexes], and [semanticIndexCallback] arguments must not be
660 /// null.
661 const SliverChildListDelegate.fixed(
662 this.children, {
663 this.addAutomaticKeepAlives = true,
664 this.addRepaintBoundaries = true,
665 this.addSemanticIndexes = true,
666 this.semanticIndexCallback = _kDefaultSemanticIndexCallback,
667 this.semanticIndexOffset = 0,
668 }) : _keyToIndex = null;
669
670 /// {@macro flutter.widgets.SliverChildBuilderDelegate.addAutomaticKeepAlives}
671 final bool addAutomaticKeepAlives;
672
673 /// {@macro flutter.widgets.SliverChildBuilderDelegate.addRepaintBoundaries}
674 final bool addRepaintBoundaries;
675
676 /// {@macro flutter.widgets.SliverChildBuilderDelegate.addSemanticIndexes}
677 final bool addSemanticIndexes;
678
679 /// {@macro flutter.widgets.SliverChildBuilderDelegate.semanticIndexOffset}
680 final int semanticIndexOffset;
681
682 /// {@macro flutter.widgets.SliverChildBuilderDelegate.semanticIndexCallback}
683 final SemanticIndexCallback semanticIndexCallback;
684
685 /// The widgets to display.
686 ///
687 /// If this list is going to be mutated, it is usually wise to put a [Key] on
688 /// each of the child widgets, so that the framework can match old
689 /// configurations to new configurations and maintain the underlying render
690 /// objects.
691 ///
692 /// Also, a [Widget] in Flutter is immutable, so directly modifying the
693 /// [children] such as `someWidget.children.add(...)` or
694 /// passing a reference of the original list value to the [children] parameter
695 /// will result in incorrect behaviors. Whenever the
696 /// children list is modified, a new list object must be provided.
697 ///
698 /// The following code corrects the problem mentioned above.
699 ///
700 /// ```dart
701 /// class SomeWidgetState extends State<SomeWidget> {
702 /// final List<Widget> _children = <Widget>[];
703 ///
704 /// void someHandler() {
705 /// setState(() {
706 /// // The key here allows Flutter to reuse the underlying render
707 /// // objects even if the children list is recreated.
708 /// _children.add(ChildWidget(key: UniqueKey()));
709 /// });
710 /// }
711 ///
712 /// @override
713 /// Widget build(BuildContext context) {
714 /// // Always create a new list of children as a Widget is immutable.
715 /// return PageView(children: List<Widget>.of(_children));
716 /// }
717 /// }
718 /// ```
719 final List<Widget> children;
720
721 // A map to cache key to index lookup for children.
722 //
723 // _keyToIndex[null] is used as current index during the lazy loading process
724 // in [_findChildIndex]. _keyToIndex should never be used for looking up null key.
725 final Map<Key?, int>? _keyToIndex;
726
727 bool get _isConstantInstance => _keyToIndex == null;
728
729 int? _findChildIndex(Key key) {
730 if (_isConstantInstance) {
731 return null;
732 }
733 // Lazily fill the [_keyToIndex].
734 if (!_keyToIndex!.containsKey(key)) {
735 int index = _keyToIndex[null]!;
736 while (index < children.length) {
737 final Widget child = children[index];
738 if (child.key != null) {
739 _keyToIndex[child.key] = index;
740 }
741 if (child.key == key) {
742 // Record current index for next function call.
743 _keyToIndex[null] = index + 1;
744 return index;
745 }
746 index += 1;
747 }
748 _keyToIndex[null] = index;
749 } else {
750 return _keyToIndex[key];
751 }
752 return null;
753 }
754
755 @override
756 int? findIndexByKey(Key key) {
757 final Key childKey;
758 if (key is _SaltedValueKey) {
759 final _SaltedValueKey saltedValueKey = key;
760 childKey = saltedValueKey.value;
761 } else {
762 childKey = key;
763 }
764 return _findChildIndex(childKey);
765 }
766
767 @override
768 Widget? build(BuildContext context, int index) {
769 if (index < 0 || index >= children.length) {
770 return null;
771 }
772 Widget child = children[index];
773 final Key? key = child.key != null ? _SaltedValueKey(child.key!) : null;
774 if (addRepaintBoundaries) {
775 child = RepaintBoundary(child: child);
776 }
777 if (addSemanticIndexes) {
778 final int? semanticIndex = semanticIndexCallback(child, index);
779 if (semanticIndex != null) {
780 child = IndexedSemantics(index: semanticIndex + semanticIndexOffset, child: child);
781 }
782 }
783 if (addAutomaticKeepAlives) {
784 child = AutomaticKeepAlive(child: _SelectionKeepAlive(child: child));
785 }
786
787 return KeyedSubtree(key: key, child: child);
788 }
789
790 @override
791 int? get estimatedChildCount => children.length;
792
793 @override
794 bool shouldRebuild(covariant SliverChildListDelegate oldDelegate) {
795 return children != oldDelegate.children;
796 }
797}
798
799class _SelectionKeepAlive extends StatefulWidget {
800 /// Creates a widget that listens to [KeepAliveNotification]s and maintains a
801 /// [KeepAlive] widget appropriately.
802 const _SelectionKeepAlive({required this.child});
803
804 /// The widget below this widget in the tree.
805 ///
806 /// {@macro flutter.widgets.ProxyWidget.child}
807 final Widget child;
808
809 @override
810 State<_SelectionKeepAlive> createState() => _SelectionKeepAliveState();
811}
812
813class _SelectionKeepAliveState extends State<_SelectionKeepAlive>
814 with AutomaticKeepAliveClientMixin
815 implements SelectionRegistrar {
816 Set<Selectable>? _selectablesWithSelections;
817 Map<Selectable, VoidCallback>? _selectableAttachments;
818 SelectionRegistrar? _registrar;
819
820 @override
821 bool get wantKeepAlive => _wantKeepAlive;
822 bool _wantKeepAlive = false;
823 set wantKeepAlive(bool value) {
824 if (_wantKeepAlive != value) {
825 _wantKeepAlive = value;
826 updateKeepAlive();
827 }
828 }
829
830 VoidCallback listensTo(Selectable selectable) {
831 return () {
832 if (selectable.value.hasSelection) {
833 _updateSelectablesWithSelections(selectable, add: true);
834 } else {
835 _updateSelectablesWithSelections(selectable, add: false);
836 }
837 };
838 }
839
840 void _updateSelectablesWithSelections(Selectable selectable, {required bool add}) {
841 if (add) {
842 assert(selectable.value.hasSelection);
843 _selectablesWithSelections ??= <Selectable>{};
844 _selectablesWithSelections!.add(selectable);
845 } else {
846 _selectablesWithSelections?.remove(selectable);
847 }
848 wantKeepAlive = _selectablesWithSelections?.isNotEmpty ?? false;
849 }
850
851 @override
852 void didChangeDependencies() {
853 super.didChangeDependencies();
854 final SelectionRegistrar? newRegistrar = SelectionContainer.maybeOf(context);
855 if (_registrar != newRegistrar) {
856 if (_registrar != null) {
857 _selectableAttachments?.keys.forEach(_registrar!.remove);
858 }
859 _registrar = newRegistrar;
860 if (_registrar != null) {
861 _selectableAttachments?.keys.forEach(_registrar!.add);
862 }
863 }
864 }
865
866 @override
867 void add(Selectable selectable) {
868 final VoidCallback attachment = listensTo(selectable);
869 selectable.addListener(attachment);
870 _selectableAttachments ??= <Selectable, VoidCallback>{};
871 _selectableAttachments![selectable] = attachment;
872 _registrar!.add(selectable);
873 if (selectable.value.hasSelection) {
874 _updateSelectablesWithSelections(selectable, add: true);
875 }
876 }
877
878 @override
879 void remove(Selectable selectable) {
880 if (_selectableAttachments == null) {
881 return;
882 }
883 assert(_selectableAttachments!.containsKey(selectable));
884 final VoidCallback attachment = _selectableAttachments!.remove(selectable)!;
885 selectable.removeListener(attachment);
886 _registrar!.remove(selectable);
887 _updateSelectablesWithSelections(selectable, add: false);
888 }
889
890 @override
891 void dispose() {
892 if (_selectableAttachments != null) {
893 for (final Selectable selectable in _selectableAttachments!.keys) {
894 _registrar!.remove(selectable);
895 selectable.removeListener(_selectableAttachments![selectable]!);
896 }
897 _selectableAttachments = null;
898 }
899 _selectablesWithSelections = null;
900 super.dispose();
901 }
902
903 @override
904 Widget build(BuildContext context) {
905 super.build(context);
906 if (_registrar == null) {
907 return widget.child;
908 }
909 return SelectionRegistrarScope(registrar: this, child: widget.child);
910 }
911}
912
913// Return a Widget for the given Exception
914Widget _createErrorWidget(Object exception, StackTrace stackTrace) {
915 final FlutterErrorDetails details = FlutterErrorDetails(
916 exception: exception,
917 stack: stackTrace,
918 library: 'widgets library',
919 context: ErrorDescription('building'),
920 );
921 FlutterError.reportError(details);
922 return ErrorWidget.builder(details);
923}
924
925/// A delegate that supplies children for scrolling in two dimensions.
926///
927/// A [TwoDimensionalScrollView] lazily constructs its box children to avoid
928/// creating more children than are visible through the
929/// [TwoDimensionalViewport]. Rather than receiving children as an
930/// explicit [List], it receives its children using a
931/// [TwoDimensionalChildDelegate].
932///
933/// As a ChangeNotifier, this delegate allows subclasses to notify its listeners
934/// (typically as a subclass of [RenderTwoDimensionalViewport]) to rebuild when
935/// aspects of the delegate change. When values returned by getters or builders
936/// on this delegate change, [notifyListeners] should be called. This signals to
937/// the [RenderTwoDimensionalViewport] that the getters and builders need to be
938/// re-queried to update the layout of children in the viewport.
939///
940/// See also:
941///
942/// * [TwoDimensionalChildBuilderDelegate], an concrete subclass of this that
943/// lazily builds children on demand.
944/// * [TwoDimensionalChildListDelegate], an concrete subclass of this that
945/// uses a two dimensional array to layout children.
946abstract class TwoDimensionalChildDelegate extends ChangeNotifier {
947 /// Creates a delegate that supplies children for scrolling in two dimensions.
948 TwoDimensionalChildDelegate() {
949 if (kFlutterMemoryAllocationsEnabled) {
950 ChangeNotifier.maybeDispatchObjectCreation(this);
951 }
952 }
953
954 /// Returns the child with the given [ChildVicinity], which is described in
955 /// terms of x and y indices.
956 ///
957 /// Subclasses must implement this function and will typically wrap their
958 /// children in [RepaintBoundary] widgets.
959 ///
960 /// The values returned by this method are cached. To indicate that the
961 /// widgets have changed, a new delegate must be provided, and the new
962 /// delegate's [shouldRebuild] method must return true. Alternatively,
963 /// calling [notifyListeners] will allow the same delegate to be used.
964 Widget? build(BuildContext context, covariant ChildVicinity vicinity);
965
966 /// Called whenever a new instance of the child delegate class is
967 /// provided.
968 ///
969 /// If the new instance represents different information than the old
970 /// instance, then the method should return true, otherwise it should return
971 /// false.
972 ///
973 /// If the method returns false, then the [build] call might be optimized
974 /// away.
975 bool shouldRebuild(covariant TwoDimensionalChildDelegate oldDelegate);
976}
977
978/// A delegate that supplies children for a [TwoDimensionalScrollView] using a
979/// builder callback.
980///
981/// The widgets returned from the builder callback are automatically wrapped in
982/// [RepaintBoundary] widgets if [addRepaintBoundaries] is true
983/// (also the default).
984///
985/// See also:
986///
987/// * [TwoDimensionalChildListDelegate], which is a similar delegate that has an
988/// explicit two dimensional array of children.
989/// * [SliverChildBuilderDelegate], which is a delegate that uses a builder
990/// callback to construct the children in one dimension instead of two.
991/// * [SliverChildListDelegate], which is a delegate that has an explicit list
992/// of children in one dimension instead of two.
993class TwoDimensionalChildBuilderDelegate extends TwoDimensionalChildDelegate {
994 /// Creates a delegate that supplies children for a [TwoDimensionalScrollView]
995 /// using the given builder callback.
996 TwoDimensionalChildBuilderDelegate({
997 required this.builder,
998 int? maxXIndex,
999 int? maxYIndex,
1000 this.addRepaintBoundaries = true,
1001 this.addAutomaticKeepAlives = true,
1002 }) : assert(maxYIndex == null || maxYIndex >= -1),
1003 assert(maxXIndex == null || maxXIndex >= -1),
1004 _maxYIndex = maxYIndex,
1005 _maxXIndex = maxXIndex;
1006
1007 /// Called to build children on demand.
1008 ///
1009 /// Implementors of [RenderTwoDimensionalViewport.layoutChildSequence]
1010 /// call this builder to create the children of the viewport. For
1011 /// [ChildVicinity] indices greater than [maxXIndex] or [maxYIndex], null will
1012 /// be returned by the default [build] implementation. This default behavior
1013 /// can be changed by overriding the build method.
1014 ///
1015 /// Must return null if asked to build a widget with a [ChildVicinity] that
1016 /// does not exist.
1017 ///
1018 /// The delegate wraps the children returned by this builder in
1019 /// [RepaintBoundary] widgets if [addRepaintBoundaries] is true.
1020 final TwoDimensionalIndexedWidgetBuilder builder;
1021
1022 /// The maximum [ChildVicinity.xIndex] for children in the x axis.
1023 ///
1024 /// {@template flutter.widgets.twoDimensionalChildBuilderDelegate.maxIndex}
1025 /// For each [ChildVicinity], the child's relative location is described in
1026 /// terms of x and y indices to facilitate a consistent visitor pattern for
1027 /// all children in the viewport.
1028 ///
1029 /// This is fairly straightforward in the context of a table implementation,
1030 /// where there is usually the same number of columns in every row and vice
1031 /// versa, each aligned one after the other.
1032 ///
1033 /// When plotting children more abstractly in two dimensional space, there may
1034 /// be more x indices for a given y index than another y index. An example of
1035 /// this would be a scatter plot where there are more children at the top of
1036 /// the graph than at the bottom.
1037 ///
1038 /// If null, subclasses of [RenderTwoDimensionalViewport] can continue call on
1039 /// the [builder] until null has been returned for each known index of x and
1040 /// y. In some cases, null may not be a terminating result, such as a table
1041 /// with a merged cell spanning multiple indices. Refer to the
1042 /// [TwoDimensionalViewport] subclass to learn how this value is applied in
1043 /// the specific use case.
1044 ///
1045 /// If not null, the value must be greater than or equal to -1, where -1
1046 /// indicates there will be no children at all provided to the
1047 /// [TwoDimensionalViewport].
1048 ///
1049 /// If the value changes, the delegate will call [notifyListeners]. This
1050 /// informs the [RenderTwoDimensionalViewport] that any cached information
1051 /// from the delegate is invalid.
1052 /// {@endtemplate}
1053 ///
1054 /// This value represents the greatest x index of all [ChildVicinity]s for the
1055 /// two dimensional scroll view.
1056 ///
1057 /// See also:
1058 ///
1059 /// * [RenderTwoDimensionalViewport.buildOrObtainChildFor], the method that
1060 /// leads to calling on the delegate to build a child of the given
1061 /// [ChildVicinity].
1062 int? get maxXIndex => _maxXIndex;
1063 int? _maxXIndex;
1064 set maxXIndex(int? value) {
1065 if (value == maxXIndex) {
1066 return;
1067 }
1068 assert(value == null || value >= -1);
1069 _maxXIndex = value;
1070 notifyListeners();
1071 }
1072
1073 /// The maximum [ChildVicinity.yIndex] for children in the y axis.
1074 ///
1075 /// {@macro flutter.widgets.twoDimensionalChildBuilderDelegate.maxIndex}
1076 ///
1077 /// This value represents the greatest y index of all [ChildVicinity]s for the
1078 /// two dimensional scroll view.
1079 ///
1080 /// See also:
1081 ///
1082 /// * [RenderTwoDimensionalViewport.buildOrObtainChildFor], the method that
1083 /// leads to calling on the delegate to build a child of the given
1084 /// [ChildVicinity].
1085 int? get maxYIndex => _maxYIndex;
1086 int? _maxYIndex;
1087 set maxYIndex(int? value) {
1088 if (maxYIndex == value) {
1089 return;
1090 }
1091 assert(value == null || value >= -1);
1092 _maxYIndex = value;
1093 notifyListeners();
1094 }
1095
1096 /// {@macro flutter.widgets.SliverChildBuilderDelegate.addRepaintBoundaries}
1097 final bool addRepaintBoundaries;
1098
1099 /// {@macro flutter.widgets.SliverChildBuilderDelegate.addAutomaticKeepAlives}
1100 final bool addAutomaticKeepAlives;
1101
1102 @override
1103 Widget? build(BuildContext context, ChildVicinity vicinity) {
1104 // If we have exceeded explicit upper bounds, return null.
1105 if (vicinity.xIndex < 0 || (maxXIndex != null && vicinity.xIndex > maxXIndex!)) {
1106 return null;
1107 }
1108 if (vicinity.yIndex < 0 || (maxYIndex != null && vicinity.yIndex > maxYIndex!)) {
1109 return null;
1110 }
1111
1112 Widget? child;
1113 try {
1114 child = builder(context, vicinity);
1115 } catch (exception, stackTrace) {
1116 child = _createErrorWidget(exception, stackTrace);
1117 }
1118 if (child == null) {
1119 return null;
1120 }
1121 if (addRepaintBoundaries) {
1122 child = RepaintBoundary(child: child);
1123 }
1124 if (addAutomaticKeepAlives) {
1125 child = AutomaticKeepAlive(child: _SelectionKeepAlive(child: child));
1126 }
1127 return child;
1128 }
1129
1130 @override
1131 bool shouldRebuild(covariant TwoDimensionalChildDelegate oldDelegate) => true;
1132}
1133
1134/// A delegate that supplies children for a [TwoDimensionalViewport] using an
1135/// explicit two dimensional array.
1136///
1137/// In general, building all the widgets in advance is not efficient. It is
1138/// better to create a delegate that builds them on demand using
1139/// [TwoDimensionalChildBuilderDelegate] or by subclassing
1140/// [TwoDimensionalChildDelegate] directly.
1141///
1142/// This class is provided for the cases where either the list of children is
1143/// known well in advance (ideally the children are themselves compile-time
1144/// constants, for example), and therefore will not be built each time the
1145/// delegate itself is created, or the array is small, such that it's likely
1146/// always visible (and thus there is nothing to be gained by building it on
1147/// demand).
1148///
1149/// The widgets in the given [children] list are automatically wrapped in
1150/// [RepaintBoundary] widgets if [addRepaintBoundaries] is true
1151/// (also the default).
1152///
1153/// The [children] are accessed for each [ChildVicinity.yIndex] and
1154/// [ChildVicinity.xIndex] of the [TwoDimensionalViewport] as
1155/// `children[vicinity.yIndex][vicinity.xIndex]`.
1156///
1157/// See also:
1158///
1159/// * [TwoDimensionalChildBuilderDelegate], which is a delegate that uses a
1160/// builder callback to construct the children.
1161/// * [SliverChildBuilderDelegate], which is a delegate that uses a builder
1162/// callback to construct the children in one dimension instead of two.
1163/// * [SliverChildListDelegate], which is a delegate that has an explicit list
1164/// of children in one dimension instead of two.
1165class TwoDimensionalChildListDelegate extends TwoDimensionalChildDelegate {
1166 /// Creates a delegate that supplies children for a [TwoDimensionalScrollView].
1167 ///
1168 /// The [children] and [addRepaintBoundaries] must not be
1169 /// null.
1170 TwoDimensionalChildListDelegate({
1171 this.addRepaintBoundaries = true,
1172 this.addAutomaticKeepAlives = true,
1173 required this.children,
1174 });
1175
1176 /// The widgets to display.
1177 ///
1178 /// Also, a [Widget] in Flutter is immutable, so directly modifying the
1179 /// [children] such as `someWidget.children.add(...)` or
1180 /// passing a reference of the original list value to the [children] parameter
1181 /// will result in incorrect behaviors. Whenever the
1182 /// children list is modified, a new list object must be provided.
1183 ///
1184 /// The [children] are accessed for each [ChildVicinity.yIndex] and
1185 /// [ChildVicinity.xIndex] of the [TwoDimensionalViewport] as
1186 /// `children[vicinity.yIndex][vicinity.xIndex]`.
1187 final List<List<Widget>> children;
1188
1189 /// {@macro flutter.widgets.SliverChildBuilderDelegate.addRepaintBoundaries}
1190 final bool addRepaintBoundaries;
1191
1192 /// {@macro flutter.widgets.SliverChildBuilderDelegate.addAutomaticKeepAlives}
1193 final bool addAutomaticKeepAlives;
1194
1195 @override
1196 Widget? build(BuildContext context, ChildVicinity vicinity) {
1197 // If we have exceeded explicit upper bounds, return null.
1198 if (vicinity.yIndex < 0 || vicinity.yIndex >= children.length) {
1199 return null;
1200 }
1201 if (vicinity.xIndex < 0 || vicinity.xIndex >= children[vicinity.yIndex].length) {
1202 return null;
1203 }
1204
1205 Widget child = children[vicinity.yIndex][vicinity.xIndex];
1206 if (addRepaintBoundaries) {
1207 child = RepaintBoundary(child: child);
1208 }
1209 if (addAutomaticKeepAlives) {
1210 child = AutomaticKeepAlive(child: _SelectionKeepAlive(child: child));
1211 }
1212 return child;
1213 }
1214
1215 @override
1216 bool shouldRebuild(covariant TwoDimensionalChildListDelegate oldDelegate) {
1217 return children != oldDelegate.children;
1218 }
1219}
1220