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