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