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