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';
6library;
7
8import 'dart:async';
9
10import 'package:flutter/foundation.dart';
11import 'package:flutter/rendering.dart';
12import 'package:flutter/scheduler.dart';
13
14import 'framework.dart';
15import 'notification_listener.dart';
16import 'sliver.dart';
17
18/// Allows subtrees to request to be kept alive in lazy lists.
19///
20/// This widget is like [KeepAlive] but instead of being explicitly configured,
21/// it listens to [KeepAliveNotification] messages from the [child] and other
22/// descendants.
23///
24/// The subtree is kept alive whenever there is one or more descendant that has
25/// sent a [KeepAliveNotification] and not yet triggered its
26/// [KeepAliveNotification.handle].
27///
28/// To send these notifications, consider using [AutomaticKeepAliveClientMixin].
29///
30/// The [SliverChildBuilderDelegate] and [SliverChildListDelegate] delegates,
31/// used with [SliverList] and [SliverGrid], as well as the scroll view
32/// counterparts [ListView] and [GridView], have an `addAutomaticKeepAlives`
33/// feature, which is enabled by default. This feature inserts
34/// [AutomaticKeepAlive] widgets around each child, which in turn configure
35/// [KeepAlive] widgets in response to [KeepAliveNotification]s.
36///
37/// The same `addAutomaticKeepAlives` feature is supported by
38/// [TwoDimensionalChildBuilderDelegate] and [TwoDimensionalChildListDelegate].
39///
40/// {@tool dartpad}
41/// This sample demonstrates how to use the [AutomaticKeepAlive] widget in
42/// combination with the [AutomaticKeepAliveClientMixin] to selectively preserve
43/// the state of individual items in a scrollable list.
44///
45/// Normally, widgets in a lazily built list like [ListView.builder] are
46/// disposed of when they leave the visible area to maintain performance. This means
47/// that any state inside a [StatefulWidget] would be lost unless explicitly
48/// preserved.
49///
50/// In this example, each list item is a [StatefulWidget] that includes a
51/// counter and an increment button. To preserve the state of selected items
52/// (based on their index), the [AutomaticKeepAlive] widget and
53/// [AutomaticKeepAliveClientMixin] are used:
54///
55/// - The `wantKeepAlive` getter in the item’s state class returns true for
56/// even-indexed items, indicating that their state should be preserved.
57/// - For odd-indexed items, `wantKeepAlive` returns false, so their state is
58/// not preserved when scrolled out of view.
59///
60/// ** See code in examples/api/lib/widgets/keep_alive/automatic_keep_alive.0.dart **
61/// {@end-tool}
62///
63/// See also:
64///
65/// * [AutomaticKeepAliveClientMixin], which is a mixin with convenience
66/// methods for clients of [AutomaticKeepAlive]. Used with [State]
67/// subclasses.
68/// * [KeepAlive] which marks a child as needing to stay alive even when it's
69/// in a lazy list that would otherwise remove it.
70class AutomaticKeepAlive extends StatefulWidget {
71 /// Creates a widget that listens to [KeepAliveNotification]s and maintains a
72 /// [KeepAlive] widget appropriately.
73 const AutomaticKeepAlive({super.key, required this.child});
74
75 /// The widget below this widget in the tree.
76 ///
77 /// {@macro flutter.widgets.ProxyWidget.child}
78 final Widget child;
79
80 @override
81 State<AutomaticKeepAlive> createState() => _AutomaticKeepAliveState();
82}
83
84class _AutomaticKeepAliveState extends State<AutomaticKeepAlive> {
85 Map<Listenable, VoidCallback>? _handles;
86 // In order to apply parent data out of turn, the child of the KeepAlive
87 // widget must be the same across frames.
88 late Widget _child;
89 bool _keepingAlive = false;
90
91 @override
92 void initState() {
93 super.initState();
94 _updateChild();
95 }
96
97 @override
98 void didUpdateWidget(AutomaticKeepAlive oldWidget) {
99 super.didUpdateWidget(oldWidget);
100 _updateChild();
101 }
102
103 void _updateChild() {
104 _child = NotificationListener<KeepAliveNotification>(
105 onNotification: _addClient,
106 child: widget.child,
107 );
108 }
109
110 @override
111 void dispose() {
112 if (_handles != null) {
113 for (final Listenable handle in _handles!.keys) {
114 handle.removeListener(_handles![handle]!);
115 }
116 }
117 super.dispose();
118 }
119
120 bool _addClient(KeepAliveNotification notification) {
121 final Listenable handle = notification.handle;
122 _handles ??= <Listenable, VoidCallback>{};
123 assert(!_handles!.containsKey(handle));
124 _handles![handle] = _createCallback(handle);
125 handle.addListener(_handles![handle]!);
126 if (!_keepingAlive) {
127 _keepingAlive = true;
128 final ParentDataElement<KeepAliveParentDataMixin>? childElement = _getChildElement();
129 if (childElement != null) {
130 // If the child already exists, update it synchronously.
131 _updateParentDataOfChild(childElement);
132 } else {
133 // If the child doesn't exist yet, we got called during the very first
134 // build of this subtree. Wait until the end of the frame to update
135 // the child when the child is guaranteed to be present.
136 SchedulerBinding.instance.addPostFrameCallback((Duration timeStamp) {
137 if (!mounted) {
138 return;
139 }
140 final ParentDataElement<KeepAliveParentDataMixin>? childElement = _getChildElement();
141 assert(childElement != null);
142 _updateParentDataOfChild(childElement!);
143 }, debugLabel: 'AutomaticKeepAlive.updateParentData');
144 }
145 }
146 return false;
147 }
148
149 /// Get the [Element] for the only [KeepAlive] child.
150 ///
151 /// While this widget is guaranteed to have a child, this may return null if
152 /// the first build of that child has not completed yet.
153 ParentDataElement<KeepAliveParentDataMixin>? _getChildElement() {
154 assert(mounted);
155 final Element element = context as Element;
156 Element? childElement;
157 // We use Element.visitChildren rather than context.visitChildElements
158 // because we might be called during build, and context.visitChildElements
159 // verifies that it is not called during build. Element.visitChildren does
160 // not, instead it assumes that the caller will be careful. (See the
161 // documentation for these methods for more details.)
162 //
163 // Here we know it's safe (with the exception outlined below) because we
164 // just received a notification, which we wouldn't be able to do if we
165 // hadn't built our child and its child -- our build method always builds
166 // the same subtree and it always includes the node we're looking for
167 // (KeepAlive) as the parent of the node that reports the notifications
168 // (NotificationListener).
169 //
170 // If we are called during the first build of this subtree the links to the
171 // children will not be hooked up yet. In that case this method returns
172 // null despite the fact that we will have a child after the build
173 // completes. It's the caller's responsibility to deal with this case.
174 //
175 // (We're only going down one level, to get our direct child.)
176 element.visitChildren((Element child) {
177 childElement = child;
178 });
179 assert(childElement == null || childElement is ParentDataElement<KeepAliveParentDataMixin>);
180 return childElement as ParentDataElement<KeepAliveParentDataMixin>?;
181 }
182
183 void _updateParentDataOfChild(ParentDataElement<KeepAliveParentDataMixin> childElement) {
184 childElement.applyWidgetOutOfTurn(build(context) as ParentDataWidget<KeepAliveParentDataMixin>);
185 }
186
187 VoidCallback _createCallback(Listenable handle) {
188 late final VoidCallback callback;
189 return callback = () {
190 assert(() {
191 if (!mounted) {
192 throw FlutterError(
193 'AutomaticKeepAlive handle triggered after AutomaticKeepAlive was disposed.\n'
194 'Widgets should always trigger their KeepAliveNotification handle when they are '
195 'deactivated, so that they (or their handle) do not send spurious events later '
196 'when they are no longer in the tree.',
197 );
198 }
199 return true;
200 }());
201 _handles!.remove(handle);
202 handle.removeListener(callback);
203 if (_handles!.isEmpty) {
204 if (SchedulerBinding.instance.schedulerPhase.index <
205 SchedulerPhase.persistentCallbacks.index) {
206 // Build/layout haven't started yet so let's just schedule this for
207 // the next frame.
208 setState(() {
209 _keepingAlive = false;
210 });
211 } else {
212 // We were probably notified by a descendant when they were yanked out
213 // of our subtree somehow. We're probably in the middle of build or
214 // layout, so there's really nothing we can do to clean up this mess
215 // short of just scheduling another build to do the cleanup. This is
216 // very unfortunate, and means (for instance) that garbage collection
217 // of these resources won't happen for another 16ms.
218 //
219 // The problem is there's really no way for us to distinguish these
220 // cases:
221 //
222 // * We haven't built yet (or missed out chance to build), but
223 // someone above us notified our descendant and our descendant is
224 // disconnecting from us. If we could mark ourselves dirty we would
225 // be able to clean everything this frame. (This is a pretty
226 // unlikely scenario in practice. Usually things change before
227 // build/layout, not during build/layout.)
228 //
229 // * Our child changed, and as our old child went away, it notified
230 // us. We can't setState, since we _just_ built. We can't apply the
231 // parent data information to our child because we don't _have_ a
232 // child at this instant. We really want to be able to change our
233 // mind about how we built, so we can give the KeepAlive widget a
234 // new value, but it's too late.
235 //
236 // * A deep descendant in another build scope just got yanked, and in
237 // the process notified us. We could apply new parent data
238 // information, but it may or may not get applied this frame,
239 // depending on whether said child is in the same layout scope.
240 //
241 // * A descendant is being moved from one position under us to
242 // another position under us. They just notified us of the removal,
243 // at some point in the future they will notify us of the addition.
244 // We don't want to do anything. (This is why we check that
245 // _handles is still empty below.)
246 //
247 // * We're being notified in the paint phase, or even in a post-frame
248 // callback. Either way it is far too late for us to make our
249 // parent lay out again this frame, so the garbage won't get
250 // collected this frame.
251 //
252 // * We are being torn out of the tree ourselves, as is our
253 // descendant, and it notified us while it was being deactivated.
254 // We don't need to do anything, but we don't know yet because we
255 // haven't been deactivated yet. (This is why we check mounted
256 // below before calling setState.)
257 //
258 // Long story short, we have to schedule a new frame and request a
259 // frame there, but this is generally a bad practice, and you should
260 // avoid it if possible.
261 _keepingAlive = false;
262 scheduleMicrotask(() {
263 if (mounted && _handles!.isEmpty) {
264 // If mounted is false, we went away as well, so there's nothing to do.
265 // If _handles is no longer empty, then another client (or the same
266 // client in a new place) registered itself before we had a chance to
267 // turn off keepalive, so again there's nothing to do.
268 setState(() {
269 assert(!_keepingAlive);
270 });
271 }
272 });
273 }
274 }
275 };
276 }
277
278 @override
279 Widget build(BuildContext context) {
280 return KeepAlive(keepAlive: _keepingAlive, child: _child);
281 }
282
283 @override
284 void debugFillProperties(DiagnosticPropertiesBuilder description) {
285 super.debugFillProperties(description);
286 description.add(
287 FlagProperty('_keepingAlive', value: _keepingAlive, ifTrue: 'keeping subtree alive'),
288 );
289 description.add(
290 DiagnosticsProperty<Map<Listenable, VoidCallback>>(
291 'handles',
292 _handles,
293 description: _handles != null
294 ? '${_handles!.length} active client${_handles!.length == 1 ? "" : "s"}'
295 : null,
296 ifNull: 'no notifications ever received',
297 ),
298 );
299 }
300}
301
302/// Indicates that the subtree through which this notification bubbles must be
303/// kept alive even if it would normally be discarded as an optimization.
304///
305/// For example, a focused text field might fire this notification to indicate
306/// that it should not be disposed even if the user scrolls the field off
307/// screen.
308///
309/// Each [KeepAliveNotification] is configured with a [handle] that consists of
310/// a [Listenable] that is triggered when the subtree no longer needs to be kept
311/// alive.
312///
313/// The [handle] should be triggered any time the sending widget is removed from
314/// the tree (in [State.deactivate]). If the widget is then rebuilt and still
315/// needs to be kept alive, it should immediately send a new notification
316/// (possible with the very same [Listenable]) during build.
317///
318/// This notification is listened to by the [AutomaticKeepAlive] widget, which
319/// is added to the tree automatically by [SliverList] (and [ListView]) and
320/// [SliverGrid] (and [GridView]) widgets.
321///
322/// Failure to trigger the [handle] in the manner described above will likely
323/// cause the [AutomaticKeepAlive] to lose track of whether the widget should be
324/// kept alive or not, leading to memory leaks or lost data. For example, if the
325/// widget that requested keepalive is removed from the subtree but doesn't
326/// trigger its [Listenable] on the way out, then the subtree will continue to
327/// be kept alive until the list itself is disposed. Similarly, if the
328/// [Listenable] is triggered while the widget needs to be kept alive, but a new
329/// [KeepAliveNotification] is not immediately sent, then the widget risks being
330/// garbage collected while it wants to be kept alive.
331///
332/// It is an error to use the same [handle] in two [KeepAliveNotification]s
333/// within the same [AutomaticKeepAlive] without triggering that [handle] before
334/// the second notification is sent.
335///
336/// For a more convenient way to interact with [AutomaticKeepAlive] widgets,
337/// consider using [AutomaticKeepAliveClientMixin], which uses
338/// [KeepAliveNotification] internally.
339class KeepAliveNotification extends Notification {
340 /// Creates a notification to indicate that a subtree must be kept alive.
341 const KeepAliveNotification(this.handle);
342
343 /// A [Listenable] that will inform its clients when the widget that fired the
344 /// notification no longer needs to be kept alive.
345 ///
346 /// The [Listenable] should be triggered any time the sending widget is
347 /// removed from the tree (in [State.deactivate]). If the widget is then
348 /// rebuilt and still needs to be kept alive, it should immediately send a new
349 /// notification (possible with the very same [Listenable]) during build.
350 ///
351 /// See also:
352 ///
353 /// * [KeepAliveHandle], a convenience class for use with this property.
354 final Listenable handle;
355}
356
357/// A [Listenable] which can be manually triggered.
358///
359/// Used with [KeepAliveNotification] objects as their
360/// [KeepAliveNotification.handle].
361///
362/// For a more convenient way to interact with [AutomaticKeepAlive] widgets,
363/// consider using [AutomaticKeepAliveClientMixin], which uses a
364/// [KeepAliveHandle] internally.
365class KeepAliveHandle extends ChangeNotifier {
366 @override
367 void dispose() {
368 notifyListeners();
369 super.dispose();
370 }
371}
372
373/// A mixin with convenience methods for clients of [AutomaticKeepAlive]. It is used
374/// with [State] subclasses to manage keep-alive behavior in lazily built lists.
375///
376/// This mixin simplifies interaction with [AutomaticKeepAlive] by automatically
377/// sending [KeepAliveNotification]s when necessary. Subclasses must implement
378/// [wantKeepAlive] to indicate whether the widget should be kept alive and call
379/// [updateKeepAlive] whenever its value changes.
380///
381/// The mixin internally manages a [KeepAliveHandle], which is used to notify
382/// the nearest [AutomaticKeepAlive] ancestor of changes in keep-alive
383/// requirements. [AutomaticKeepAlive] listens for [KeepAliveNotification]s sent
384/// by this mixin and dynamically wraps the subtree in a [KeepAlive] widget to
385/// preserve its state when it is no longer visible in the viewport.
386///
387/// Subclasses must implement [wantKeepAlive], and their [build] methods must
388/// call `super.build` (though the return value should be ignored).
389///
390/// Then, whenever [wantKeepAlive]'s value changes (or might change), the
391/// subclass should call [updateKeepAlive].
392///
393/// The type argument `T` is the type of the [StatefulWidget] subclass of the
394/// [State] into which this class is being mixed.
395///
396/// The [SliverChildBuilderDelegate] and [SliverChildListDelegate] delegates,
397/// used with [SliverList] and [SliverGrid], as well as the scroll view
398/// counterparts [ListView] and [GridView], have an `addAutomaticKeepAlives`
399/// feature, which is enabled by default. This feature inserts
400/// [AutomaticKeepAlive] widgets around each child, which in turn configure
401/// [KeepAlive] widgets in response to [KeepAliveNotification]s.
402///
403/// The same `addAutomaticKeepAlives` feature is supported by
404/// [TwoDimensionalChildBuilderDelegate] and [TwoDimensionalChildListDelegate].
405///
406/// {@tool dartpad}
407/// This example demonstrates how to use the
408/// [AutomaticKeepAliveClientMixin] to keep the state of a widget alive even
409/// when it is scrolled out of view.
410///
411/// ** See code in examples/api/lib/widgets/keep_alive/automatic_keep_alive_client_mixin.0.dart **
412/// {@end-tool}
413///
414/// See also:
415///
416/// * [AutomaticKeepAlive], which listens to messages from this mixin.
417/// * [KeepAliveNotification], the notifications sent by this mixin.
418/// * [KeepAlive] which marks a child as needing to stay alive even when it's
419/// in a lazy list that would otherwise remove it.
420@optionalTypeArgs
421mixin AutomaticKeepAliveClientMixin<T extends StatefulWidget> on State<T> {
422 KeepAliveHandle? _keepAliveHandle;
423
424 void _ensureKeepAlive() {
425 assert(_keepAliveHandle == null);
426 _keepAliveHandle = KeepAliveHandle();
427 KeepAliveNotification(_keepAliveHandle!).dispatch(context);
428 }
429
430 void _releaseKeepAlive() {
431 _keepAliveHandle!.dispose();
432 _keepAliveHandle = null;
433 }
434
435 /// Whether the current instance should be kept alive.
436 ///
437 /// Call [updateKeepAlive] whenever this getter's value changes.
438 @protected
439 bool get wantKeepAlive;
440
441 /// Ensures that any [AutomaticKeepAlive] ancestors are in a good state, by
442 /// firing a [KeepAliveNotification] or triggering the [KeepAliveHandle] as
443 /// appropriate.
444 @protected
445 void updateKeepAlive() {
446 if (wantKeepAlive) {
447 if (_keepAliveHandle == null) {
448 _ensureKeepAlive();
449 }
450 } else {
451 if (_keepAliveHandle != null) {
452 _releaseKeepAlive();
453 }
454 }
455 }
456
457 @override
458 void initState() {
459 super.initState();
460 if (wantKeepAlive) {
461 _ensureKeepAlive();
462 }
463 }
464
465 @override
466 void deactivate() {
467 if (_keepAliveHandle != null) {
468 _releaseKeepAlive();
469 }
470 super.deactivate();
471 }
472
473 @mustCallSuper
474 @override
475 Widget build(BuildContext context) {
476 if (wantKeepAlive && _keepAliveHandle == null) {
477 _ensureKeepAlive();
478 // Whenever wantKeepAlive's value changes (or might change), the
479 // subclass should call [updateKeepAlive].
480 // That will ensure that the keepalive is disabled (or enabled)
481 // without requiring a rebuild.
482 }
483 return const _NullWidget();
484 }
485}
486
487class _NullWidget extends StatelessWidget {
488 const _NullWidget();
489
490 @override
491 Widget build(BuildContext context) {
492 throw FlutterError(
493 'Widgets that mix AutomaticKeepAliveClientMixin into their State must '
494 'call super.build() but must ignore the return value of the superclass.',
495 );
496 }
497}
498