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'; |
6 | library; |
7 | |
8 | import 'dart:async'; |
9 | |
10 | import 'package:flutter/foundation.dart'; |
11 | import 'package:flutter/rendering.dart'; |
12 | import 'package:flutter/scheduler.dart'; |
13 | |
14 | import 'framework.dart'; |
15 | import 'notification_listener.dart'; |
16 | import '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 | class 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 | |
46 | class _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. |
298 | class 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. |
324 | class 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 |
349 | mixin 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 | |
415 | class _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 |
Definitions
- AutomaticKeepAlive
- AutomaticKeepAlive
- createState
- _AutomaticKeepAliveState
- initState
- didUpdateWidget
- _updateChild
- dispose
- _addClient
- _getChildElement
- _updateParentDataOfChild
- _createCallback
- build
- debugFillProperties
- KeepAliveNotification
- KeepAliveNotification
- KeepAliveHandle
- dispose
- AutomaticKeepAliveClientMixin
- _ensureKeepAlive
- _releaseKeepAlive
- wantKeepAlive
- updateKeepAlive
- initState
- deactivate
- build
- _NullWidget
- _NullWidget
Learn more about Flutter for embedded and desktop on industrialflutter.com