automatic_keep_alive_test.dart 30 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
2 3 4
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

5
import 'package:flutter/foundation.dart';
6
import 'package:flutter/gestures.dart' show DragStartBehavior;
7
import 'package:flutter/rendering.dart';
8 9 10 11
import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart';

class Leaf extends StatefulWidget {
12
  const Leaf({ required Key key, required this.child }) : super(key: key);
13 14
  final Widget child;
  @override
15
  State<Leaf> createState() => _LeafState();
16 17 18 19
}

class _LeafState extends State<Leaf> {
  bool _keepAlive = false;
20
  KeepAliveHandle? _handle;
21 22 23

  @override
  void deactivate() {
24
    _handle?.dispose();
25 26 27 28 29 30 31 32
    _handle = null;
    super.deactivate();
  }

  void setKeepAlive(bool value) {
    _keepAlive = value;
    if (_keepAlive) {
      if (_handle == null) {
33
        _handle = KeepAliveHandle();
34
        KeepAliveNotification(_handle!).dispatch(context);
35 36
      }
    } else {
37
      _handle?.dispose();
38 39 40 41 42 43 44
      _handle = null;
    }
  }

  @override
  Widget build(BuildContext context) {
    if (_keepAlive && _handle == null) {
45
      _handle = KeepAliveHandle();
46
      KeepAliveNotification(_handle!).dispatch(context);
47 48 49 50 51
    }
    return widget.child;
  }
}

52
List<Widget> generateList(Widget child, { required bool impliedMode }) {
53
  return List<Widget>.generate(
54 55
    100,
    (int index) {
56 57
      final Widget result = Leaf(
        key: GlobalObjectKey<_LeafState>(index),
58 59
        child: child,
      );
60
      if (impliedMode) {
61
        return result;
62
      }
63
      return AutomaticKeepAlive(child: result);
64 65 66 67 68
    },
    growable: false,
  );
}

69
void tests({ required bool impliedMode }) {
70
  testWidgets('AutomaticKeepAlive with ListView with itemExtent', (WidgetTester tester) async {
71
    await tester.pumpWidget(
72
      Directionality(
73
        textDirection: TextDirection.ltr,
74
        child: ListView(
75 76
          addAutomaticKeepAlives: impliedMode,
          addRepaintBoundaries: impliedMode,
77
          addSemanticIndexes: false,
78
          itemExtent: 12.3, // about 50 widgets visible
79
          cacheExtent: 0.0,
80 81 82 83
          children: generateList(const Placeholder(), impliedMode: impliedMode),
        ),
      ),
    );
84 85
    expect(find.byKey(const GlobalObjectKey<_LeafState>(3)), findsOneWidget);
    expect(find.byKey(const GlobalObjectKey<_LeafState>(30)), findsOneWidget);
86 87 88 89
    expect(find.byKey(const GlobalObjectKey<_LeafState>(59), skipOffstage: false), findsNothing);
    expect(find.byKey(const GlobalObjectKey<_LeafState>(60), skipOffstage: false), findsNothing);
    expect(find.byKey(const GlobalObjectKey<_LeafState>(61), skipOffstage: false), findsNothing);
    expect(find.byKey(const GlobalObjectKey<_LeafState>(90), skipOffstage: false), findsNothing);
90 91
    await tester.drag(find.byType(ListView), const Offset(0.0, -300.0)); // about 25 widgets' worth
    await tester.pump();
92
    expect(find.byKey(const GlobalObjectKey<_LeafState>(3), skipOffstage: false), findsNothing);
93 94 95 96
    expect(find.byKey(const GlobalObjectKey<_LeafState>(30)), findsOneWidget);
    expect(find.byKey(const GlobalObjectKey<_LeafState>(59)), findsOneWidget);
    expect(find.byKey(const GlobalObjectKey<_LeafState>(60)), findsOneWidget);
    expect(find.byKey(const GlobalObjectKey<_LeafState>(61)), findsOneWidget);
97
    expect(find.byKey(const GlobalObjectKey<_LeafState>(90), skipOffstage: false), findsNothing);
98
    const GlobalObjectKey<_LeafState>(60).currentState!.setKeepAlive(true);
99 100 101 102
    await tester.drag(find.byType(ListView), const Offset(0.0, 300.0)); // back to top
    await tester.pump();
    expect(find.byKey(const GlobalObjectKey<_LeafState>(3)), findsOneWidget);
    expect(find.byKey(const GlobalObjectKey<_LeafState>(30)), findsOneWidget);
103 104 105 106 107
    expect(find.byKey(const GlobalObjectKey<_LeafState>(59), skipOffstage: false), findsNothing);
    expect(find.byKey(const GlobalObjectKey<_LeafState>(60), skipOffstage: false), findsOneWidget);
    expect(find.byKey(const GlobalObjectKey<_LeafState>(60)), findsNothing);
    expect(find.byKey(const GlobalObjectKey<_LeafState>(61), skipOffstage: false), findsNothing);
    expect(find.byKey(const GlobalObjectKey<_LeafState>(90), skipOffstage: false), findsNothing);
108
    const GlobalObjectKey<_LeafState>(60).currentState!.setKeepAlive(false);
109 110 111
    await tester.pump();
    expect(find.byKey(const GlobalObjectKey<_LeafState>(3)), findsOneWidget);
    expect(find.byKey(const GlobalObjectKey<_LeafState>(30)), findsOneWidget);
112 113 114 115
    expect(find.byKey(const GlobalObjectKey<_LeafState>(59), skipOffstage: false), findsNothing);
    expect(find.byKey(const GlobalObjectKey<_LeafState>(60), skipOffstage: false), findsNothing);
    expect(find.byKey(const GlobalObjectKey<_LeafState>(61), skipOffstage: false), findsNothing);
    expect(find.byKey(const GlobalObjectKey<_LeafState>(90), skipOffstage: false), findsNothing);
116 117
  });

118
  testWidgets('AutomaticKeepAlive with ListView without itemExtent', (WidgetTester tester) async {
119
    await tester.pumpWidget(
120
      Directionality(
121
        textDirection: TextDirection.ltr,
122
        child: ListView(
123 124
          addAutomaticKeepAlives: impliedMode,
          addRepaintBoundaries: impliedMode,
125
          addSemanticIndexes: false,
126
          cacheExtent: 0.0,
127
          children: generateList(
128
            const SizedBox(height: 12.3, child: Placeholder()), // about 50 widgets visible
129 130 131
            impliedMode: impliedMode,
          ),
        ),
132
      ),
133
    );
134 135
    expect(find.byKey(const GlobalObjectKey<_LeafState>(3)), findsOneWidget);
    expect(find.byKey(const GlobalObjectKey<_LeafState>(30)), findsOneWidget);
136 137 138 139
    expect(find.byKey(const GlobalObjectKey<_LeafState>(59), skipOffstage: false), findsNothing);
    expect(find.byKey(const GlobalObjectKey<_LeafState>(60), skipOffstage: false), findsNothing);
    expect(find.byKey(const GlobalObjectKey<_LeafState>(61), skipOffstage: false), findsNothing);
    expect(find.byKey(const GlobalObjectKey<_LeafState>(90), skipOffstage: false), findsNothing);
140 141
    await tester.drag(find.byType(ListView), const Offset(0.0, -300.0)); // about 25 widgets' worth
    await tester.pump();
142
    expect(find.byKey(const GlobalObjectKey<_LeafState>(3), skipOffstage: false), findsNothing);
143 144 145 146
    expect(find.byKey(const GlobalObjectKey<_LeafState>(30)), findsOneWidget);
    expect(find.byKey(const GlobalObjectKey<_LeafState>(59)), findsOneWidget);
    expect(find.byKey(const GlobalObjectKey<_LeafState>(60)), findsOneWidget);
    expect(find.byKey(const GlobalObjectKey<_LeafState>(61)), findsOneWidget);
147
    expect(find.byKey(const GlobalObjectKey<_LeafState>(90), skipOffstage: false), findsNothing);
148
    const GlobalObjectKey<_LeafState>(60).currentState!.setKeepAlive(true);
149 150 151 152
    await tester.drag(find.byType(ListView), const Offset(0.0, 300.0)); // back to top
    await tester.pump();
    expect(find.byKey(const GlobalObjectKey<_LeafState>(3)), findsOneWidget);
    expect(find.byKey(const GlobalObjectKey<_LeafState>(30)), findsOneWidget);
153 154 155 156 157
    expect(find.byKey(const GlobalObjectKey<_LeafState>(59), skipOffstage: false), findsNothing);
    expect(find.byKey(const GlobalObjectKey<_LeafState>(60), skipOffstage: false), findsOneWidget);
    expect(find.byKey(const GlobalObjectKey<_LeafState>(60)), findsNothing);
    expect(find.byKey(const GlobalObjectKey<_LeafState>(61), skipOffstage: false), findsNothing);
    expect(find.byKey(const GlobalObjectKey<_LeafState>(90), skipOffstage: false), findsNothing);
158
    const GlobalObjectKey<_LeafState>(60).currentState!.setKeepAlive(false);
159 160 161
    await tester.pump();
    expect(find.byKey(const GlobalObjectKey<_LeafState>(3)), findsOneWidget);
    expect(find.byKey(const GlobalObjectKey<_LeafState>(30)), findsOneWidget);
162 163 164 165
    expect(find.byKey(const GlobalObjectKey<_LeafState>(59), skipOffstage: false), findsNothing);
    expect(find.byKey(const GlobalObjectKey<_LeafState>(60), skipOffstage: false), findsNothing);
    expect(find.byKey(const GlobalObjectKey<_LeafState>(61), skipOffstage: false), findsNothing);
    expect(find.byKey(const GlobalObjectKey<_LeafState>(90), skipOffstage: false), findsNothing);
166 167
  });

168
  testWidgets('AutomaticKeepAlive with GridView', (WidgetTester tester) async {
169
    await tester.pumpWidget(
170
      Directionality(
171
        textDirection: TextDirection.ltr,
172
        child: GridView.count(
173 174
          addAutomaticKeepAlives: impliedMode,
          addRepaintBoundaries: impliedMode,
175
          addSemanticIndexes: false,
176 177
          crossAxisCount: 2,
          childAspectRatio: 400.0 / 24.6, // about 50 widgets visible
178
          cacheExtent: 0.0,
179
          children: generateList(
180
            const Placeholder(),
181 182 183
            impliedMode: impliedMode,
          ),
        ),
184
      ),
185
    );
186 187
    expect(find.byKey(const GlobalObjectKey<_LeafState>(3)), findsOneWidget);
    expect(find.byKey(const GlobalObjectKey<_LeafState>(30)), findsOneWidget);
188 189 190 191
    expect(find.byKey(const GlobalObjectKey<_LeafState>(59), skipOffstage: false), findsNothing);
    expect(find.byKey(const GlobalObjectKey<_LeafState>(60), skipOffstage: false), findsNothing);
    expect(find.byKey(const GlobalObjectKey<_LeafState>(61), skipOffstage: false), findsNothing);
    expect(find.byKey(const GlobalObjectKey<_LeafState>(90), skipOffstage: false), findsNothing);
192 193
    await tester.drag(find.byType(GridView), const Offset(0.0, -300.0)); // about 25 widgets' worth
    await tester.pump();
194
    expect(find.byKey(const GlobalObjectKey<_LeafState>(3), skipOffstage: false), findsNothing);
195 196 197 198
    expect(find.byKey(const GlobalObjectKey<_LeafState>(30)), findsOneWidget);
    expect(find.byKey(const GlobalObjectKey<_LeafState>(59)), findsOneWidget);
    expect(find.byKey(const GlobalObjectKey<_LeafState>(60)), findsOneWidget);
    expect(find.byKey(const GlobalObjectKey<_LeafState>(61)), findsOneWidget);
199
    expect(find.byKey(const GlobalObjectKey<_LeafState>(90), skipOffstage: false), findsNothing);
200
    const GlobalObjectKey<_LeafState>(60).currentState!.setKeepAlive(true);
201 202 203 204
    await tester.drag(find.byType(GridView), const Offset(0.0, 300.0)); // back to top
    await tester.pump();
    expect(find.byKey(const GlobalObjectKey<_LeafState>(3)), findsOneWidget);
    expect(find.byKey(const GlobalObjectKey<_LeafState>(30)), findsOneWidget);
205 206 207 208 209
    expect(find.byKey(const GlobalObjectKey<_LeafState>(59), skipOffstage: false), findsNothing);
    expect(find.byKey(const GlobalObjectKey<_LeafState>(60), skipOffstage: false), findsOneWidget);
    expect(find.byKey(const GlobalObjectKey<_LeafState>(60)), findsNothing);
    expect(find.byKey(const GlobalObjectKey<_LeafState>(61), skipOffstage: false), findsNothing);
    expect(find.byKey(const GlobalObjectKey<_LeafState>(90), skipOffstage: false), findsNothing);
210
    const GlobalObjectKey<_LeafState>(60).currentState!.setKeepAlive(false);
211 212 213
    await tester.pump();
    expect(find.byKey(const GlobalObjectKey<_LeafState>(3)), findsOneWidget);
    expect(find.byKey(const GlobalObjectKey<_LeafState>(30)), findsOneWidget);
214 215 216 217
    expect(find.byKey(const GlobalObjectKey<_LeafState>(59), skipOffstage: false), findsNothing);
    expect(find.byKey(const GlobalObjectKey<_LeafState>(60), skipOffstage: false), findsNothing);
    expect(find.byKey(const GlobalObjectKey<_LeafState>(61), skipOffstage: false), findsNothing);
    expect(find.byKey(const GlobalObjectKey<_LeafState>(90), skipOffstage: false), findsNothing);
218 219 220 221 222 223 224
  });
}

void main() {
  group('Explicit automatic keep-alive', () { tests(impliedMode: false); });
  group('Implied automatic keep-alive', () { tests(impliedMode: true); });

225
  testWidgets('AutomaticKeepAlive double', (WidgetTester tester) async {
226
    await tester.pumpWidget(
227
      Directionality(
228
        textDirection: TextDirection.ltr,
229
        child: ListView(
230 231
          addAutomaticKeepAlives: false,
          addRepaintBoundaries: false,
232
          addSemanticIndexes: false,
233
          cacheExtent: 0.0,
234
          children: const <Widget>[
235
            AutomaticKeepAlive(
236
              child: SizedBox(
237
                height: 400.0,
238
                child: Stack(children: <Widget>[
239 240
                  Leaf(key: GlobalObjectKey<_LeafState>(0), child: Placeholder()),
                  Leaf(key: GlobalObjectKey<_LeafState>(1), child: Placeholder()),
241 242
                ]),
              ),
243
            ),
244
            AutomaticKeepAlive(
245 246
              child: SizedBox(
                key: GlobalObjectKey<_LeafState>(2),
247 248
                height: 400.0,
              ),
249
            ),
250
            AutomaticKeepAlive(
251 252
              child: SizedBox(
                key: GlobalObjectKey<_LeafState>(3),
253 254
                height: 400.0,
              ),
255
            ),
256 257
          ],
        ),
258
      ),
259
    );
260 261 262
    expect(find.byKey(const GlobalObjectKey<_LeafState>(0)), findsOneWidget);
    expect(find.byKey(const GlobalObjectKey<_LeafState>(1)), findsOneWidget);
    expect(find.byKey(const GlobalObjectKey<_LeafState>(2)), findsOneWidget);
263
    expect(find.byKey(const GlobalObjectKey<_LeafState>(3), skipOffstage: false), findsNothing);
264 265
    await tester.drag(find.byType(ListView), const Offset(0.0, -1000.0)); // move to bottom
    await tester.pump();
266 267
    expect(find.byKey(const GlobalObjectKey<_LeafState>(0), skipOffstage: false), findsNothing);
    expect(find.byKey(const GlobalObjectKey<_LeafState>(1), skipOffstage: false), findsNothing);
268 269 270 271 272 273 274
    expect(find.byKey(const GlobalObjectKey<_LeafState>(2)), findsOneWidget);
    expect(find.byKey(const GlobalObjectKey<_LeafState>(3)), findsOneWidget);
    await tester.drag(find.byType(ListView), const Offset(0.0, 1000.0)); // move to top
    await tester.pump();
    expect(find.byKey(const GlobalObjectKey<_LeafState>(0)), findsOneWidget);
    expect(find.byKey(const GlobalObjectKey<_LeafState>(1)), findsOneWidget);
    expect(find.byKey(const GlobalObjectKey<_LeafState>(2)), findsOneWidget);
275
    expect(find.byKey(const GlobalObjectKey<_LeafState>(3), skipOffstage: false), findsNothing);
276
    const GlobalObjectKey<_LeafState>(0).currentState!.setKeepAlive(true);
277 278
    await tester.drag(find.byType(ListView), const Offset(0.0, -1000.0)); // move to bottom
    await tester.pump();
279 280 281 282
    expect(find.byKey(const GlobalObjectKey<_LeafState>(0), skipOffstage: false), findsOneWidget);
    expect(find.byKey(const GlobalObjectKey<_LeafState>(1), skipOffstage: false), findsOneWidget);
    expect(find.byKey(const GlobalObjectKey<_LeafState>(0)), findsNothing);
    expect(find.byKey(const GlobalObjectKey<_LeafState>(1)), findsNothing);
283 284
    expect(find.byKey(const GlobalObjectKey<_LeafState>(2)), findsOneWidget);
    expect(find.byKey(const GlobalObjectKey<_LeafState>(3)), findsOneWidget);
285
    const GlobalObjectKey<_LeafState>(1).currentState!.setKeepAlive(true);
286
    await tester.pump();
287 288 289 290
    expect(find.byKey(const GlobalObjectKey<_LeafState>(0), skipOffstage: false), findsOneWidget);
    expect(find.byKey(const GlobalObjectKey<_LeafState>(1), skipOffstage: false), findsOneWidget);
    expect(find.byKey(const GlobalObjectKey<_LeafState>(0)), findsNothing);
    expect(find.byKey(const GlobalObjectKey<_LeafState>(1)), findsNothing);
291 292
    expect(find.byKey(const GlobalObjectKey<_LeafState>(2)), findsOneWidget);
    expect(find.byKey(const GlobalObjectKey<_LeafState>(3)), findsOneWidget);
293
    const GlobalObjectKey<_LeafState>(0).currentState!.setKeepAlive(false);
294
    await tester.pump();
295 296 297 298
    expect(find.byKey(const GlobalObjectKey<_LeafState>(0), skipOffstage: false), findsOneWidget);
    expect(find.byKey(const GlobalObjectKey<_LeafState>(1), skipOffstage: false), findsOneWidget);
    expect(find.byKey(const GlobalObjectKey<_LeafState>(0)), findsNothing);
    expect(find.byKey(const GlobalObjectKey<_LeafState>(1)), findsNothing);
299 300
    expect(find.byKey(const GlobalObjectKey<_LeafState>(2)), findsOneWidget);
    expect(find.byKey(const GlobalObjectKey<_LeafState>(3)), findsOneWidget);
301
    const GlobalObjectKey<_LeafState>(1).currentState!.setKeepAlive(false);
302
    await tester.pump();
303 304
    expect(find.byKey(const GlobalObjectKey<_LeafState>(0), skipOffstage: false), findsNothing);
    expect(find.byKey(const GlobalObjectKey<_LeafState>(1), skipOffstage: false), findsNothing);
305 306 307 308
    expect(find.byKey(const GlobalObjectKey<_LeafState>(2)), findsOneWidget);
    expect(find.byKey(const GlobalObjectKey<_LeafState>(3)), findsOneWidget);
  });

309
  testWidgets('AutomaticKeepAlive double 2', (WidgetTester tester) async {
310
    await tester.pumpWidget(
311
      Directionality(
312
        textDirection: TextDirection.ltr,
313
        child: ListView(
314 315
          addAutomaticKeepAlives: false,
          addRepaintBoundaries: false,
316
          addSemanticIndexes: false,
317
          cacheExtent: 0.0,
318
          children: const <Widget>[
319
            AutomaticKeepAlive(
320
              child: SizedBox(
321
                height: 400.0,
322
                child: Stack(children: <Widget>[
323 324
                  Leaf(key: GlobalObjectKey<_LeafState>(0), child: Placeholder()),
                  Leaf(key: GlobalObjectKey<_LeafState>(1), child: Placeholder()),
325 326
                ]),
              ),
327
            ),
328
            AutomaticKeepAlive(
329
              child: SizedBox(
330
                height: 400.0,
331
                child: Stack(children: <Widget>[
332 333
                  Leaf(key: GlobalObjectKey<_LeafState>(2), child: Placeholder()),
                  Leaf(key: GlobalObjectKey<_LeafState>(3), child: Placeholder()),
334 335
                ]),
              ),
336
            ),
337
            AutomaticKeepAlive(
338
              child: SizedBox(
339
                height: 400.0,
340
                child: Stack(children: <Widget>[
341 342
                  Leaf(key: GlobalObjectKey<_LeafState>(4), child: Placeholder()),
                  Leaf(key: GlobalObjectKey<_LeafState>(5), child: Placeholder()),
343 344
                ]),
              ),
345
            ),
346 347
          ],
        ),
348
      ),
349
    );
350 351 352 353
    expect(find.byKey(const GlobalObjectKey<_LeafState>(0)), findsOneWidget);
    expect(find.byKey(const GlobalObjectKey<_LeafState>(1)), findsOneWidget);
    expect(find.byKey(const GlobalObjectKey<_LeafState>(2)), findsOneWidget);
    expect(find.byKey(const GlobalObjectKey<_LeafState>(3)), findsOneWidget);
354 355
    expect(find.byKey(const GlobalObjectKey<_LeafState>(4), skipOffstage: false), findsNothing);
    expect(find.byKey(const GlobalObjectKey<_LeafState>(5), skipOffstage: false), findsNothing);
356
    const GlobalObjectKey<_LeafState>(0).currentState!.setKeepAlive(true);
357 358
    await tester.drag(find.byType(ListView), const Offset(0.0, -1000.0)); // move to bottom
    await tester.pump();
359 360 361 362
    expect(find.byKey(const GlobalObjectKey<_LeafState>(0), skipOffstage: false), findsOneWidget);
    expect(find.byKey(const GlobalObjectKey<_LeafState>(1), skipOffstage: false), findsOneWidget);
    expect(find.byKey(const GlobalObjectKey<_LeafState>(1)), findsNothing);
    expect(find.byKey(const GlobalObjectKey<_LeafState>(0)), findsNothing);
363 364 365 366
    expect(find.byKey(const GlobalObjectKey<_LeafState>(2)), findsOneWidget);
    expect(find.byKey(const GlobalObjectKey<_LeafState>(3)), findsOneWidget);
    expect(find.byKey(const GlobalObjectKey<_LeafState>(4)), findsOneWidget);
    expect(find.byKey(const GlobalObjectKey<_LeafState>(5)), findsOneWidget);
367
    await tester.pumpWidget(Directionality(
368
      textDirection: TextDirection.ltr,
369
      child: ListView(
370 371
        addAutomaticKeepAlives: false,
        addRepaintBoundaries: false,
372
        addSemanticIndexes: false,
373
        cacheExtent: 0.0,
374
        children: const <Widget>[
375
          AutomaticKeepAlive(
376
            child: SizedBox(
377
              height: 400.0,
378
              child: Stack(children: <Widget>[
379
                Leaf(key: GlobalObjectKey<_LeafState>(1), child: Placeholder()),
380 381
              ]),
            ),
382
          ),
383
          AutomaticKeepAlive(
384
            child: SizedBox(
385
              height: 400.0,
386
              child: Stack(children: <Widget>[
387 388
                Leaf(key: GlobalObjectKey<_LeafState>(2), child: Placeholder()),
                Leaf(key: GlobalObjectKey<_LeafState>(3), child: Placeholder()),
389 390
              ]),
            ),
391
          ),
392
          AutomaticKeepAlive(
393
            child: SizedBox(
394
              height: 400.0,
395
              child: Stack(children: <Widget>[
396 397 398
                Leaf(key: GlobalObjectKey<_LeafState>(4), child: Placeholder()),
                Leaf(key: GlobalObjectKey<_LeafState>(5), child: Placeholder()),
                Leaf(key: GlobalObjectKey<_LeafState>(0), child: Placeholder()),
399 400
              ]),
            ),
401
          ),
402 403
        ],
      ),
404 405
    ));
    await tester.pump(); // Sometimes AutomaticKeepAlive needs an extra pump to clean things up.
406
    expect(find.byKey(const GlobalObjectKey<_LeafState>(1), skipOffstage: false), findsNothing);
407 408 409 410 411 412 413 414 415 416
    expect(find.byKey(const GlobalObjectKey<_LeafState>(2)), findsOneWidget);
    expect(find.byKey(const GlobalObjectKey<_LeafState>(3)), findsOneWidget);
    expect(find.byKey(const GlobalObjectKey<_LeafState>(4)), findsOneWidget);
    expect(find.byKey(const GlobalObjectKey<_LeafState>(5)), findsOneWidget);
    expect(find.byKey(const GlobalObjectKey<_LeafState>(0)), findsOneWidget);
    await tester.drag(find.byType(ListView), const Offset(0.0, 1000.0)); // move to top
    await tester.pump();
    expect(find.byKey(const GlobalObjectKey<_LeafState>(1)), findsOneWidget);
    expect(find.byKey(const GlobalObjectKey<_LeafState>(2)), findsOneWidget);
    expect(find.byKey(const GlobalObjectKey<_LeafState>(3)), findsOneWidget);
417 418 419 420 421 422
    expect(find.byKey(const GlobalObjectKey<_LeafState>(4), skipOffstage: false), findsOneWidget);
    expect(find.byKey(const GlobalObjectKey<_LeafState>(5), skipOffstage: false), findsOneWidget);
    expect(find.byKey(const GlobalObjectKey<_LeafState>(0), skipOffstage: false), findsOneWidget);
    expect(find.byKey(const GlobalObjectKey<_LeafState>(4)), findsNothing);
    expect(find.byKey(const GlobalObjectKey<_LeafState>(5)), findsNothing);
    expect(find.byKey(const GlobalObjectKey<_LeafState>(0)), findsNothing);
423
    const GlobalObjectKey<_LeafState>(0).currentState!.setKeepAlive(false);
424 425 426 427
    await tester.pump();
    expect(find.byKey(const GlobalObjectKey<_LeafState>(1)), findsOneWidget);
    expect(find.byKey(const GlobalObjectKey<_LeafState>(2)), findsOneWidget);
    expect(find.byKey(const GlobalObjectKey<_LeafState>(3)), findsOneWidget);
428 429 430
    expect(find.byKey(const GlobalObjectKey<_LeafState>(4), skipOffstage: false), findsNothing);
    expect(find.byKey(const GlobalObjectKey<_LeafState>(5), skipOffstage: false), findsNothing);
    expect(find.byKey(const GlobalObjectKey<_LeafState>(0), skipOffstage: false), findsNothing);
431
    await tester.pumpWidget(Directionality(
432
      textDirection: TextDirection.ltr,
433
      child: ListView(
434 435
        addAutomaticKeepAlives: false,
        addRepaintBoundaries: false,
436
        addSemanticIndexes: false,
437
        cacheExtent: 0.0,
438
        children: const <Widget>[
439
          AutomaticKeepAlive(
440
            child: SizedBox(
441
              height: 400.0,
442
              child: Stack(children: <Widget>[
443 444
                Leaf(key: GlobalObjectKey<_LeafState>(1), child: Placeholder()),
                Leaf(key: GlobalObjectKey<_LeafState>(2), child: Placeholder()),
445 446
              ]),
            ),
447
          ),
448
          AutomaticKeepAlive(
449
            child: SizedBox(
450
              height: 400.0,
451
              child: Stack(),
452
            ),
453
          ),
454
          AutomaticKeepAlive(
455
            child: SizedBox(
456
              height: 400.0,
457
              child: Stack(children: <Widget>[
458 459 460 461
                Leaf(key: GlobalObjectKey<_LeafState>(3), child: Placeholder()),
                Leaf(key: GlobalObjectKey<_LeafState>(4), child: Placeholder()),
                Leaf(key: GlobalObjectKey<_LeafState>(5), child: Placeholder()),
                Leaf(key: GlobalObjectKey<_LeafState>(0), child: Placeholder()),
462 463
              ]),
            ),
464
          ),
465 466
        ],
      ),
467 468 469 470
    ));
    await tester.pump(); // Sometimes AutomaticKeepAlive needs an extra pump to clean things up.
    expect(find.byKey(const GlobalObjectKey<_LeafState>(1)), findsOneWidget);
    expect(find.byKey(const GlobalObjectKey<_LeafState>(2)), findsOneWidget);
471 472 473 474
    expect(find.byKey(const GlobalObjectKey<_LeafState>(3), skipOffstage: false), findsNothing);
    expect(find.byKey(const GlobalObjectKey<_LeafState>(4), skipOffstage: false), findsNothing);
    expect(find.byKey(const GlobalObjectKey<_LeafState>(5), skipOffstage: false), findsNothing);
    expect(find.byKey(const GlobalObjectKey<_LeafState>(0), skipOffstage: false), findsNothing);
475
  });
476

477
  testWidgets('AutomaticKeepAlive with keepAlive set to true before initState', (WidgetTester tester) async {
478
    await tester.pumpWidget(Directionality(
479
      textDirection: TextDirection.ltr,
480
      child: ListView.builder(
481
        dragStartBehavior: DragStartBehavior.down,
482
        addSemanticIndexes: false,
483
        itemCount: 50,
484
        itemBuilder: (BuildContext context, int index) {
485
          if (index == 0) {
486
            return const _AlwaysKeepAlive(
487
              key: GlobalObjectKey<_AlwaysKeepAliveState>(0),
488 489
            );
          }
490
          return SizedBox(
491
            height: 44.0,
492
            child: Text('FooBar $index'),
493 494 495 496 497 498 499 500 501 502 503 504
          );
        },
      ),
    ));

    expect(find.text('keep me alive'), findsOneWidget);
    expect(find.text('FooBar 1'), findsOneWidget);
    expect(find.text('FooBar 2'), findsOneWidget);

    expect(find.byKey(const GlobalObjectKey<_AlwaysKeepAliveState>(0)), findsOneWidget);
    await tester.drag(find.byType(ListView), const Offset(0.0, -1000.0)); // move to bottom
    await tester.pump();
505
    expect(find.byKey(const GlobalObjectKey<_AlwaysKeepAliveState>(0), skipOffstage: false), findsOneWidget);
506

507
    expect(find.text('keep me alive', skipOffstage: false), findsOneWidget);
508 509 510
    expect(find.text('FooBar 1'), findsNothing);
    expect(find.text('FooBar 2'), findsNothing);
  });
511

512
  testWidgets('AutomaticKeepAlive with keepAlive set to true before initState and widget goes out of scope', (WidgetTester tester) async {
513
    await tester.pumpWidget(Directionality(
514
      textDirection: TextDirection.ltr,
515
      child: ListView.builder(
516
        addSemanticIndexes: false,
517
        itemCount: 250,
518
        itemBuilder: (BuildContext context, int index) {
519
          if (index.isEven) {
520
            return _AlwaysKeepAlive(
521 522 523
              key: GlobalObjectKey<_AlwaysKeepAliveState>(index),
            );
          }
524
          return SizedBox(
525
            height: 44.0,
526
            child: Text('FooBar $index'),
527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549
          );
        },
      ),
    ));

    expect(find.text('keep me alive'), findsNWidgets(7));
    expect(find.text('FooBar 1'), findsOneWidget);
    expect(find.text('FooBar 3'), findsOneWidget);

    expect(find.byKey(const GlobalObjectKey<_AlwaysKeepAliveState>(0)), findsOneWidget);

    final ScrollableState state = tester.state(find.byType(Scrollable));
    final ScrollPosition position = state.position;
    position.jumpTo(3025.0);

    await tester.pump();
    expect(find.byKey(const GlobalObjectKey<_AlwaysKeepAliveState>(0), skipOffstage: false), findsOneWidget);

    expect(find.text('keep me alive', skipOffstage: false), findsNWidgets(23));
    expect(find.text('FooBar 1'), findsNothing);
    expect(find.text('FooBar 3'), findsNothing);
    expect(find.text('FooBar 73'), findsOneWidget);
  });
550

551
  testWidgets('AutomaticKeepAlive with SliverKeepAliveWidget', (WidgetTester tester) async {
552 553 554 555
    // We're just doing a basic test here to make sure that the functionality of
    // RenderSliverWithKeepAliveMixin doesn't get regressed or deleted. As testing
    // the full functionality would be cumbersome.
    final RenderSliverMultiBoxAdaptorAlt alternate = RenderSliverMultiBoxAdaptorAlt();
556
    addTearDown(alternate.dispose);
557
    final RenderBox child = RenderBoxKeepAlive();
558
    addTearDown(child.dispose);
559 560 561 562
    alternate.insert(child);

    expect(alternate.children.length, 1);
  });
563

564
  testWidgets('Keep alive Listenable has its listener removed once called', (WidgetTester tester) async {
565
    final LeakCheckerHandle handle = LeakCheckerHandle();
566
    addTearDown(handle.dispose);
567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583
    await tester.pumpWidget(Directionality(
      textDirection: TextDirection.ltr,
      child: ListView.builder(
        itemCount: 1,
        itemBuilder: (BuildContext context, int index) {
          return const KeepAliveListenableLeakChecker(key: GlobalObjectKey<_KeepAliveListenableLeakCheckerState>(0));
        },
      ),
    ));
    final _KeepAliveListenableLeakCheckerState state = const GlobalObjectKey<_KeepAliveListenableLeakCheckerState>(0).currentState!;

    expect(handle.hasListeners, false);
    state.dispatch(handle);
    expect(handle.hasListeners, true);
    handle.notifyListeners();
    expect(handle.hasListeners, false);
  });
584 585 586
}

class _AlwaysKeepAlive extends StatefulWidget {
587
  const _AlwaysKeepAlive({ required Key key }) : super(key: key);
588 589

  @override
590
  State<StatefulWidget> createState() => _AlwaysKeepAliveState();
591 592 593 594 595 596 597 598 599
}

class _AlwaysKeepAliveState extends State<_AlwaysKeepAlive> with AutomaticKeepAliveClientMixin<_AlwaysKeepAlive> {
  @override
  bool get wantKeepAlive => true;

  @override
  Widget build(BuildContext context) {
    super.build(context);
600
    return const SizedBox(
601
      height: 48.0,
602
      child: Text('keep me alive'),
603 604 605
    );
  }
}
606

607
class RenderBoxKeepAlive extends RenderBox { }
608

609
mixin KeepAliveParentDataMixinAlt implements KeepAliveParentDataMixin {
610 611 612 613 614 615 616 617 618 619 620 621 622 623
  @override
  bool keptAlive = false;

  @override
  bool keepAlive = false;
}

class RenderSliverMultiBoxAdaptorAlt extends RenderSliver with
    KeepAliveParentDataMixinAlt,
    RenderSliverHelpers,
    RenderSliverWithKeepAliveMixin {

  final List<RenderBox> children = <RenderBox>[];

624
  void insert(RenderBox child, { RenderBox? after }) {
625 626 627 628 629 630 631 632 633
    children.add(child);
  }

  @override
  void visitChildren(RenderObjectVisitor visitor) {
    children.forEach(visitor);
  }

  @override
634
  void performLayout() { }
635
}
636 637

class LeakCheckerHandle with ChangeNotifier {
638 639
  LeakCheckerHandle() {
    if (kFlutterMemoryAllocationsEnabled) {
640
      ChangeNotifier.maybeDispatchObjectCreation(this);
641 642 643
    }
  }

644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664
  @override
  bool get hasListeners => super.hasListeners;
}

class KeepAliveListenableLeakChecker extends StatefulWidget {
  const KeepAliveListenableLeakChecker({super.key});

  @override
  State<KeepAliveListenableLeakChecker> createState() => _KeepAliveListenableLeakCheckerState();
}

class _KeepAliveListenableLeakCheckerState extends State<KeepAliveListenableLeakChecker> {
  void dispatch(Listenable handle) {
    KeepAliveNotification(handle).dispatch(context);
  }

  @override
  Widget build(BuildContext context) {
    return const Placeholder();
  }
}