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

Provided by KDAB

Privacy Policy
Learn more about Flutter for embedded and desktop on industrialflutter.com