automatic_keep_alive.dart 16.4 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
2 3 4 5 6 7
// 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';
8
import 'package:flutter/rendering.dart';
9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
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({
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 43
}

class _AutomaticKeepAliveState extends State<AutomaticKeepAlive> {
44
  Map<Listenable, VoidCallback>? _handles;
45 46
  // In order to apply parent data out of turn, the child of the KeepAlive
  // widget must be the same across frames.
47
  late Widget _child;
48 49 50 51 52 53 54 55 56 57 58 59 60 61 62
  bool _keepingAlive = false;

  @override
  void initState() {
    super.initState();
    _updateChild();
  }

  @override
  void didUpdateWidget(AutomaticKeepAlive oldWidget) {
    super.didUpdateWidget(oldWidget);
    _updateChild();
  }

  void _updateChild() {
63
    _child = NotificationListener<KeepAliveNotification>(
64
      onNotification: _addClient,
65
      child: widget.child,
66 67 68 69 70 71
    );
  }

  @override
  void dispose() {
    if (_handles != null) {
72
      for (final Listenable handle in _handles!.keys) {
73
        handle.removeListener(_handles![handle]!);
74
      }
75 76 77 78 79 80 81
    }
    super.dispose();
  }

  bool _addClient(KeepAliveNotification notification) {
    final Listenable handle = notification.handle;
    _handles ??= <Listenable, VoidCallback>{};
82 83 84
    assert(!_handles!.containsKey(handle));
    _handles![handle] = _createCallback(handle);
    handle.addListener(_handles![handle]!);
85 86
    if (!_keepingAlive) {
      _keepingAlive = true;
87
      final ParentDataElement<KeepAliveParentDataMixin>? childElement = _getChildElement();
88 89 90 91 92 93 94
      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.
95
        SchedulerBinding.instance.addPostFrameCallback((Duration timeStamp) {
96 97 98
          if (!mounted) {
            return;
          }
99
          final ParentDataElement<KeepAliveParentDataMixin>? childElement = _getChildElement();
100
          assert(childElement != null);
101
          _updateParentDataOfChild(childElement!);
102 103
        });
      }
104 105 106 107
    }
    return false;
  }

108 109 110 111
  /// 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.
112
  ParentDataElement<KeepAliveParentDataMixin>? _getChildElement() {
113
    assert(mounted);
114
    final Element element = context as Element;
115
    Element? childElement;
116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137
    // 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;
    });
138
    assert(childElement == null || childElement is ParentDataElement<KeepAliveParentDataMixin>);
139
    return childElement as ParentDataElement<KeepAliveParentDataMixin>?;
140 141
  }

142 143
  void _updateParentDataOfChild(ParentDataElement<KeepAliveParentDataMixin> childElement) {
    childElement.applyWidgetOutOfTurn(build(context) as ParentDataWidget<KeepAliveParentDataMixin>);
144 145
  }

146
  VoidCallback _createCallback(Listenable handle) {
147 148
    late final VoidCallback callback;
    return callback = () {
149 150
      assert(() {
        if (!mounted) {
151
          throw FlutterError(
152
            'AutomaticKeepAlive handle triggered after AutomaticKeepAlive was disposed.\n'
153 154
            'Widgets should always trigger their KeepAliveNotification handle when they are '
            '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 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
          // 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(() {
219
            if (mounted && _handles!.isEmpty) {
220 221 222
              // 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
223
              // turn off keepalive, so again there's nothing to do.
224 225 226 227 228 229 230 231 232 233 234 235
              setState(() {
                assert(!_keepingAlive);
              });
            }
          });
        }
      }
    };
  }

  @override
  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 247
    description.add(FlagProperty('_keepingAlive', value: _keepingAlive, ifTrue: 'keeping subtree alive'));
    description.add(DiagnosticsProperty<Map<Listenable, VoidCallback>>(
248 249 250
      'handles',
      _handles,
      description: _handles != null ?
251
        '${_handles!.length} active client${ _handles!.length == 1 ? "" : "s" }' :
252 253 254
        null,
      ifNull: 'no notifications ever received',
    ));
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 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280
///
/// 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
281
/// widget that requested keepalive is removed from the subtree but doesn't
282 283 284 285 286 287 288 289 290 291 292 293 294 295 296
/// 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.
297
  const KeepAliveNotification(this.handle);
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

  /// 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.
324 325 326 327 328 329 330
  ///
  /// This method does not call [dispose]. When the handle is not needed
  /// anymore, it must be [dispose]d regardless of whether notifying listeners.
  @Deprecated(
    'Use dispose instead. '
    'This feature was deprecated after v3.3.0-0.0.pre.',
  )
331 332 333
  void release() {
    notifyListeners();
  }
334 335 336 337 338 339

  @override
  void dispose() {
    notifyListeners();
    super.dispose();
  }
340 341
}

Ian Hickson's avatar
Ian Hickson committed
342 343
/// A mixin with convenience methods for clients of [AutomaticKeepAlive]. Used
/// 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 349 350
///
/// Then, whenever [wantKeepAlive]'s value changes (or might change), the
/// subclass should call [updateKeepAlive].
///
Ian Hickson's avatar
Ian Hickson committed
351 352 353
/// The type argument `T` is the type of the [StatefulWidget] subclass of the
/// [State] into which this class is being mixed.
///
354 355 356 357
/// See also:
///
///  * [AutomaticKeepAlive], which listens to messages from this mixin.
///  * [KeepAliveNotification], the notifications sent by this mixin.
358
@optionalTypeArgs
359
mixin AutomaticKeepAliveClientMixin<T extends StatefulWidget> on State<T> {
360
  KeepAliveHandle? _keepAliveHandle;
361 362 363

  void _ensureKeepAlive() {
    assert(_keepAliveHandle == null);
364
    _keepAliveHandle = KeepAliveHandle();
365
    KeepAliveNotification(_keepAliveHandle!).dispatch(context);
366 367 368
  }

  void _releaseKeepAlive() {
369 370
    // Dispose and release do not imply each other.
    _keepAliveHandle!.dispose();
371 372 373 374 375 376 377 378 379 380 381 382 383 384 385
    _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) {
386
      if (_keepAliveHandle == null) {
387
        _ensureKeepAlive();
388
      }
389
    } else {
390
      if (_keepAliveHandle != null) {
391
        _releaseKeepAlive();
392
      }
393 394 395 396 397 398
    }
  }

  @override
  void initState() {
    super.initState();
399
    if (wantKeepAlive) {
400
      _ensureKeepAlive();
401
    }
402 403 404 405
  }

  @override
  void deactivate() {
406
    if (_keepAliveHandle != null) {
407
      _releaseKeepAlive();
408
    }
409 410 411 412 413 414
    super.deactivate();
  }

  @mustCallSuper
  @override
  Widget build(BuildContext context) {
415
    if (wantKeepAlive && _keepAliveHandle == null) {
416
      _ensureKeepAlive();
417
    }
418 419 420 421 422 423 424 425 426 427 428
    return const _NullWidget();
  }
}

class _NullWidget extends StatelessWidget {
  const _NullWidget();

  @override
  Widget build(BuildContext context) {
    throw FlutterError(
      'Widgets that mix AutomaticKeepAliveClientMixin into their State must '
429
      'call super.build() but must ignore the return value of the superclass.',
430
    );
431
  }
432
}