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