automatic_keep_alive.dart 15.5 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 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
// 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/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.
34 35
  ///
  /// {@macro flutter.widgets.child}
36 37 38
  final Widget child;

  @override
39
  _AutomaticKeepAliveState createState() => _AutomaticKeepAliveState();
40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59
}

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() {
60
    _child = NotificationListener<KeepAliveNotification>(
61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82
      onNotification: _addClient,
      child: widget.child,
    );
  }

  @override
  void dispose() {
    if (_handles != null) {
      for (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;
83
      final ParentDataElement<SliverWithKeepAliveWidget> childElement = _getChildElement();
84 85 86 87 88 89 90 91
      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) {
92 93 94
          if (!mounted) {
            return;
          }
95
          final ParentDataElement<SliverWithKeepAliveWidget> childElement = _getChildElement();
96 97 98 99
          assert(childElement != null);
          _updateParentDataOfChild(childElement);
        });
      }
100 101 102 103
    }
    return false;
  }

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

138
  void _updateParentDataOfChild(ParentDataElement<SliverWithKeepAliveWidget> childElement) {
139
    childElement.applyWidgetOutOfTurn(build(context) as ParentDataWidget<SliverWithKeepAliveWidget>);
140 141
  }

142 143 144 145
  VoidCallback _createCallback(Listenable handle) {
    return () {
      assert(() {
        if (!mounted) {
146
          throw FlutterError(
147 148 149 150 151 152 153
            'AutomaticKeepAlive handle triggered after AutomaticKeepAlive was disposed.'
            '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;
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
      _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
217
              // turn off keepalive, so again there's nothing to do.
218 219 220 221 222 223 224 225 226 227 228 229 230
              setState(() {
                assert(!_keepingAlive);
              });
            }
          });
        }
      }
    };
  }

  @override
  Widget build(BuildContext context) {
    assert(_child != null);
231
    return KeepAlive(
232 233 234 235 236 237 238
      keepAlive: _keepingAlive,
      child: _child,
    );
  }


  @override
239
  void debugFillProperties(DiagnosticPropertiesBuilder description) {
240
    super.debugFillProperties(description);
241 242
    description.add(FlagProperty('_keepingAlive', value: _keepingAlive, ifTrue: 'keeping subtree alive'));
    description.add(DiagnosticsProperty<Map<Listenable, VoidCallback>>(
243 244 245 246 247 248 249
      'handles',
      _handles,
      description: _handles != null ?
        '${_handles.length} active client${ _handles.length == 1 ? "" : "s" }' :
        null,
      ifNull: 'no notifications ever received',
    ));
250 251 252 253
  }
}

/// Indicates that the subtree through which this notification bubbles must be
254
/// kept alive even if it would normally be discarded as an optimization.
255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275
///
/// 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
276
/// widget that requested keepalive is removed from the subtree but doesn't
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
/// 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();
  }
}

Ian Hickson's avatar
Ian Hickson committed
326 327
/// A mixin with convenience methods for clients of [AutomaticKeepAlive]. Used
/// with [State] subclasses.
328 329
///
/// Subclasses must implement [wantKeepAlive], and their [build] methods must
Ian Hickson's avatar
Ian Hickson committed
330 331
/// call `super.build` (the return value will always return null, and should be
/// ignored).
332 333 334 335
///
/// Then, whenever [wantKeepAlive]'s value changes (or might change), the
/// subclass should call [updateKeepAlive].
///
Ian Hickson's avatar
Ian Hickson committed
336 337 338
/// The type argument `T` is the type of the [StatefulWidget] subclass of the
/// [State] into which this class is being mixed.
///
339 340 341 342
/// See also:
///
///  * [AutomaticKeepAlive], which listens to messages from this mixin.
///  * [KeepAliveNotification], the notifications sent by this mixin.
343
@optionalTypeArgs
344
mixin AutomaticKeepAliveClientMixin<T extends StatefulWidget> on State<T> {
345 346 347 348
  KeepAliveHandle _keepAliveHandle;

  void _ensureKeepAlive() {
    assert(_keepAliveHandle == null);
349 350
    _keepAliveHandle = KeepAliveHandle();
    KeepAliveNotification(_keepAliveHandle).dispatch(context);
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
  }

  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 null;
  }
399
}