scroll_aware_image_provider_test.dart 16 KB
Newer Older
1 2 3 4
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

5 6
import 'dart:ui' as ui show Image;

7
import 'package:flutter/widgets.dart';
8
import 'package:flutter_test/flutter_test.dart';
9 10 11 12

import '../painting/image_test_utils.dart';

void main() {
13

14
  late ui.Image testImage;
15 16 17 18 19

  setUpAll(() async {
    testImage = await createTestImage(width: 10, height: 10);
  });

20
  tearDown(() {
21
    imageCache.clear();
22 23
  });

24
  T findPhysics<T extends ScrollPhysics>(WidgetTester tester) {
25
    return Scrollable.of(find.byType(TestWidget).evaluate().first).position.physics as T;
26 27
  }

28
  ScrollMetrics findMetrics(WidgetTester tester) {
29
    return Scrollable.of(find.byType(TestWidget).evaluate().first).position;
30 31 32 33 34 35
  }

  testWidgets('ScrollAwareImageProvider does not delay if widget is not in scrollable', (WidgetTester tester) async {
    final GlobalKey<TestWidgetState> key = GlobalKey<TestWidgetState>();
    await tester.pumpWidget(TestWidget(key));

36
    final DisposableBuildContext context = DisposableBuildContext(key.currentState!);
37
    final TestImageProvider testImageProvider = TestImageProvider(testImage.clone());
38 39 40 41 42 43
    final ScrollAwareImageProvider<TestImageProvider> imageProvider = ScrollAwareImageProvider<TestImageProvider>(
      context: context,
      imageProvider: testImageProvider,
    );

    expect(testImageProvider.configuration, null);
44
    expect(imageCache.containsKey(testImageProvider), false);
45 46 47 48 49

    final ImageStream stream = imageProvider.resolve(ImageConfiguration.empty);

    expect(testImageProvider.configuration, ImageConfiguration.empty);
    expect(stream.completer, isNotNull);
50
    expect(stream.completer!.hasListeners, true);
51 52
    expect(imageCache.containsKey(testImageProvider), true);
    expect(imageCache.currentSize, 0);
53 54 55

    testImageProvider.complete();

56
    expect(imageCache.currentSize, 1);
57 58 59 60 61 62 63 64 65 66 67 68 69 70
  });

  testWidgets('ScrollAwareImageProvider does not delay if in scrollable that is not scrolling', (WidgetTester tester) async {
    final GlobalKey<TestWidgetState> key = GlobalKey<TestWidgetState>();
    await tester.pumpWidget(Directionality(
      textDirection: TextDirection.ltr,
      child: ListView(
        physics: RecordingPhysics(),
        children: <Widget>[
          TestWidget(key),
        ],
      ),
    ));

71
    final DisposableBuildContext context = DisposableBuildContext(key.currentState!);
72
    final TestImageProvider testImageProvider = TestImageProvider(testImage.clone());
73 74 75 76 77 78
    final ScrollAwareImageProvider<TestImageProvider> imageProvider = ScrollAwareImageProvider<TestImageProvider>(
      context: context,
      imageProvider: testImageProvider,
    );

    expect(testImageProvider.configuration, null);
79
    expect(imageCache.containsKey(testImageProvider), false);
80 81 82 83 84

    final ImageStream stream = imageProvider.resolve(ImageConfiguration.empty);

    expect(testImageProvider.configuration, ImageConfiguration.empty);
    expect(stream.completer, isNotNull);
85
    expect(stream.completer!.hasListeners, true);
86 87
    expect(imageCache.containsKey(testImageProvider), true);
    expect(imageCache.currentSize, 0);
88 89 90

    testImageProvider.complete();

91
    expect(imageCache.currentSize, 1);
92
    expect(findPhysics<RecordingPhysics>(tester).velocities, <double>[0]);
93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110
  });

  testWidgets('ScrollAwareImageProvider does not delay if in scrollable that is scrolling slowly', (WidgetTester tester) async {
    final List<GlobalKey<TestWidgetState>> keys = <GlobalKey<TestWidgetState>>[];
    final ScrollController scrollController = ScrollController();
    await tester.pumpWidget(Directionality(
      textDirection: TextDirection.ltr,
      child: ListView.builder(
        physics: RecordingPhysics(),
        controller: scrollController,
        itemBuilder: (BuildContext context, int index) {
          keys.add(GlobalKey<TestWidgetState>());
          return TestWidget(keys.last);
        },
        itemCount: 50,
      ),
    ));

111
    final DisposableBuildContext context = DisposableBuildContext(keys.last.currentState!);
112
    final TestImageProvider testImageProvider = TestImageProvider(testImage.clone());
113 114 115 116 117 118
    final ScrollAwareImageProvider<TestImageProvider> imageProvider = ScrollAwareImageProvider<TestImageProvider>(
      context: context,
      imageProvider: testImageProvider,
    );

    expect(testImageProvider.configuration, null);
119
    expect(imageCache.containsKey(testImageProvider), false);
120 121 122 123 124 125 126

    scrollController.animateTo(
      100,
      duration: const Duration(seconds: 2),
      curve: Curves.fastLinearToSlowEaseIn,
    );
    await tester.pump();
127
    final RecordingPhysics physics = findPhysics<RecordingPhysics>(tester);
128 129 130 131 132 133 134

    expect(physics.velocities.length, 0);
    final ImageStream stream = imageProvider.resolve(ImageConfiguration.empty);
    expect(physics.velocities.length, 1);
    expect(
      const ScrollPhysics().recommendDeferredLoading(
        physics.velocities.first,
135
        findMetrics(tester),
136 137 138 139 140 141 142
        find.byType(TestWidget).evaluate().first,
      ),
      false,
    );

    expect(testImageProvider.configuration, ImageConfiguration.empty);
    expect(stream.completer, isNotNull);
143
    expect(stream.completer!.hasListeners, true);
144 145
    expect(imageCache.containsKey(testImageProvider), true);
    expect(imageCache.currentSize, 0);
146 147 148

    testImageProvider.complete();

149
    expect(imageCache.currentSize, 1);
150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167
  });

  testWidgets('ScrollAwareImageProvider delays if in scrollable that is scrolling fast', (WidgetTester tester) async {
    final List<GlobalKey<TestWidgetState>> keys = <GlobalKey<TestWidgetState>>[];
    final ScrollController scrollController = ScrollController();
    await tester.pumpWidget(Directionality(
      textDirection: TextDirection.ltr,
      child: ListView.builder(
        physics: RecordingPhysics(),
        controller: scrollController,
        itemBuilder: (BuildContext context, int index) {
          keys.add(GlobalKey<TestWidgetState>());
          return TestWidget(keys.last);
        },
        itemCount: 50,
      ),
    ));

168
    final DisposableBuildContext context = DisposableBuildContext(keys.last.currentState!);
169
    final TestImageProvider testImageProvider = TestImageProvider(testImage.clone());
170 171 172 173 174 175
    final ScrollAwareImageProvider<TestImageProvider> imageProvider = ScrollAwareImageProvider<TestImageProvider>(
      context: context,
      imageProvider: testImageProvider,
    );

    expect(testImageProvider.configuration, null);
176
    expect(imageCache.containsKey(testImageProvider), false);
177 178 179 180 181 182 183

    scrollController.animateTo(
      3000,
      duration: const Duration(seconds: 2),
      curve: Curves.fastLinearToSlowEaseIn,
    );
    await tester.pump();
184
    final RecordingPhysics physics = findPhysics<RecordingPhysics>(tester);
185 186 187 188 189 190 191

    expect(physics.velocities.length, 0);
    final ImageStream stream = imageProvider.resolve(ImageConfiguration.empty);
    expect(physics.velocities.length, 1);
    expect(
      const ScrollPhysics().recommendDeferredLoading(
        physics.velocities.first,
192
        findMetrics(tester),
193 194 195 196 197 198 199 200
        find.byType(TestWidget).evaluate().first,
      ),
      true,
    );

    expect(testImageProvider.configuration, null);
    expect(stream.completer, null);

201 202
    expect(imageCache.containsKey(testImageProvider), false);
    expect(imageCache.currentSize, 0);
203 204 205 206 207 208

    await tester.pump(const Duration(seconds: 1));
    expect(physics.velocities.last, 0);

    expect(testImageProvider.configuration, ImageConfiguration.empty);
    expect(stream.completer, isNotNull);
209
    expect(stream.completer!.hasListeners, true);
210

211 212
    expect(imageCache.containsKey(testImageProvider), true);
    expect(imageCache.currentSize, 0);
213 214 215

    testImageProvider.complete();

216
    expect(imageCache.currentSize, 1);
217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234
  });

  testWidgets('ScrollAwareImageProvider delays if in scrollable that is scrolling fast and fizzles if disposed', (WidgetTester tester) async {
    final List<GlobalKey<TestWidgetState>> keys = <GlobalKey<TestWidgetState>>[];
    final ScrollController scrollController = ScrollController();
    await tester.pumpWidget(Directionality(
      textDirection: TextDirection.ltr,
      child: ListView.builder(
        physics: RecordingPhysics(),
        controller: scrollController,
        itemBuilder: (BuildContext context, int index) {
          keys.add(GlobalKey<TestWidgetState>());
          return TestWidget(keys.last);
        },
        itemCount: 50,
      ),
    ));

235
    final DisposableBuildContext context = DisposableBuildContext(keys.last.currentState!);
236
    final TestImageProvider testImageProvider = TestImageProvider(testImage.clone());
237 238 239 240 241 242
    final ScrollAwareImageProvider<TestImageProvider> imageProvider = ScrollAwareImageProvider<TestImageProvider>(
      context: context,
      imageProvider: testImageProvider,
    );

    expect(testImageProvider.configuration, null);
243
    expect(imageCache.containsKey(testImageProvider), false);
244 245 246 247 248 249 250

    scrollController.animateTo(
      3000,
      duration: const Duration(seconds: 2),
      curve: Curves.fastLinearToSlowEaseIn,
    );
    await tester.pump();
251
    final RecordingPhysics physics = findPhysics<RecordingPhysics>(tester);
252 253 254 255 256 257 258

    expect(physics.velocities.length, 0);
    final ImageStream stream = imageProvider.resolve(ImageConfiguration.empty);
    expect(physics.velocities.length, 1);
    expect(
      const ScrollPhysics().recommendDeferredLoading(
        physics.velocities.first,
259
        findMetrics(tester),
260 261 262 263 264 265 266 267
        find.byType(TestWidget).evaluate().first,
      ),
      true,
    );

    expect(testImageProvider.configuration, null);
    expect(stream.completer, null);

268 269
    expect(imageCache.containsKey(testImageProvider), false);
    expect(imageCache.currentSize, 0);
270 271 272 273 274 275 276 277 278 279

    // as if we had picked a context that scrolled out of the tree.
    context.dispose();

    await tester.pump(const Duration(seconds: 1));
    expect(physics.velocities.length, 1);

    expect(testImageProvider.configuration, null);
    expect(stream.completer, null);

280 281
    expect(imageCache.containsKey(testImageProvider), false);
    expect(imageCache.currentSize, 0);
282 283 284

    testImageProvider.complete();

285
    expect(imageCache.currentSize, 0);
286
  });
287 288 289 290 291 292 293 294 295 296 297 298 299

  testWidgets('ScrollAwareImageProvider resolves from ImageCache and does not set completer twice', (WidgetTester tester) async {
    final GlobalKey<TestWidgetState> key = GlobalKey<TestWidgetState>();
    final ScrollController scrollController = ScrollController();
    await tester.pumpWidget(Directionality(
      textDirection: TextDirection.ltr,
      child: SingleChildScrollView(
        physics: ControllablePhysics(),
        controller: scrollController,
        child: TestWidget(key),
      ),
    ));

300
    final DisposableBuildContext context = DisposableBuildContext(key.currentState!);
301
    final TestImageProvider testImageProvider = TestImageProvider(testImage.clone());
302 303 304 305 306 307
    final ScrollAwareImageProvider<TestImageProvider> imageProvider = ScrollAwareImageProvider<TestImageProvider>(
      context: context,
      imageProvider: testImageProvider,
    );

    expect(testImageProvider.configuration, null);
308
    expect(imageCache.containsKey(testImageProvider), false);
309

310
    final ControllablePhysics physics = findPhysics<ControllablePhysics>(tester);
311 312 313 314 315 316
    physics.recommendDeferredLoadingValue = true;

    final ImageStream stream = imageProvider.resolve(ImageConfiguration.empty);

    expect(testImageProvider.configuration, null);
    expect(stream.completer, null);
317 318
    expect(imageCache.containsKey(testImageProvider), false);
    expect(imageCache.currentSize, 0);
319

320
    // Simulate a case where someone else has managed to complete this stream -
321 322 323 324
    // so it can land in the cache right before we stop scrolling fast.
    // If we miss the early return, we will fail.
    testImageProvider.complete();

325
    imageCache.putIfAbsent(testImageProvider, () => testImageProvider.loadImage(testImageProvider, PaintingBinding.instance.instantiateImageCodecWithSize));
326 327 328 329
    // We've stopped scrolling fast.
    physics.recommendDeferredLoadingValue = false;
    await tester.idle();

330 331
    expect(imageCache.containsKey(testImageProvider), true);
    expect(imageCache.currentSize, 1);
332
    expect(testImageProvider.loadCallCount, 1);
333 334
    expect(stream.completer, null);
  });
335 336

  testWidgets('ScrollAwareImageProvider does not block LRU updates to image cache', (WidgetTester tester) async {
337 338
    final int oldSize = imageCache.maximumSize;
    imageCache.maximumSize = 1;
339 340 341 342 343 344 345 346 347 348 349 350

    final GlobalKey<TestWidgetState> key = GlobalKey<TestWidgetState>();
    final ScrollController scrollController = ScrollController();
    await tester.pumpWidget(Directionality(
      textDirection: TextDirection.ltr,
      child: SingleChildScrollView(
        physics: ControllablePhysics(),
        controller: scrollController,
        child: TestWidget(key),
      ),
    ));

351
    final DisposableBuildContext context = DisposableBuildContext(key.currentState!);
352
    final TestImageProvider testImageProvider = TestImageProvider(testImage.clone());
353 354 355 356 357 358
    final ScrollAwareImageProvider<TestImageProvider> imageProvider = ScrollAwareImageProvider<TestImageProvider>(
      context: context,
      imageProvider: testImageProvider,
    );

    expect(testImageProvider.configuration, null);
359
    expect(imageCache.containsKey(testImageProvider), false);
360

361
    final ControllablePhysics physics = findPhysics<ControllablePhysics>(tester);
362 363 364 365 366 367
    physics.recommendDeferredLoadingValue = true;

    final ImageStream stream = imageProvider.resolve(ImageConfiguration.empty);

    expect(testImageProvider.configuration, null);
    expect(stream.completer, null);
368
    expect(imageCache.currentSize, 0);
369 370

    // Occupy the only slot in the cache with another image.
371
    final TestImageProvider testImageProvider2 = TestImageProvider(testImage.clone());
372
    testImageProvider2.complete();
373
    await precacheImage(testImageProvider2, context.context!);
374 375 376
    expect(imageCache.containsKey(testImageProvider), false);
    expect(imageCache.containsKey(testImageProvider2), true);
    expect(imageCache.currentSize, 1);
377 378 379

    // Complete the original image while we're still scrolling fast.
    testImageProvider.complete();
380
    stream.setCompleter(testImageProvider.loadImage(testImageProvider, PaintingBinding.instance.instantiateImageCodecWithSize));
381

382
    // Verify that this hasn't changed the cache state yet
383 384 385
    expect(imageCache.containsKey(testImageProvider), false);
    expect(imageCache.containsKey(testImageProvider2), true);
    expect(imageCache.currentSize, 1);
386 387 388 389 390 391
    expect(testImageProvider.loadCallCount, 1);

    await tester.pump();

    // After pumping a frame, the original image should be in the cache because
    // it took the LRU slot.
392 393 394
    expect(imageCache.containsKey(testImageProvider), true);
    expect(imageCache.containsKey(testImageProvider2), false);
    expect(imageCache.currentSize, 1);
395 396
    expect(testImageProvider.loadCallCount, 1);

397
    imageCache.maximumSize = oldSize;
398
  });
399 400 401
}

class TestWidget extends StatefulWidget {
402
  const TestWidget(Key? key) : super(key: key);
403 404 405 406 407 408 409 410 411 412 413

  @override
  State<TestWidget> createState() => TestWidgetState();
}

class TestWidgetState extends State<TestWidget> {
  @override
  Widget build(BuildContext context) => const SizedBox(height: 50);
}

class RecordingPhysics extends ScrollPhysics {
414
  RecordingPhysics({ super.parent });
415 416 417 418

  final List<double> velocities = <double>[];

  @override
419
  RecordingPhysics applyTo(ScrollPhysics? ancestor) {
420
    return RecordingPhysics(parent: buildParent(ancestor));
421 422 423 424 425 426 427 428
  }

  @override
  bool recommendDeferredLoading(double velocity, ScrollMetrics metrics, BuildContext context) {
    velocities.add(velocity);
    return super.recommendDeferredLoading(velocity, metrics, context);
  }
}
429 430 431 432 433

// Ignore this so that we can mutate whether we defer loading or not at specific
// times without worrying about actual scrolling mechanics.
// ignore: must_be_immutable
class ControllablePhysics extends ScrollPhysics {
434
  ControllablePhysics({ super.parent });
435 436 437 438

  bool recommendDeferredLoadingValue = false;

  @override
439
  ControllablePhysics applyTo(ScrollPhysics? ancestor) {
440
    return ControllablePhysics(parent: buildParent(ancestor));
441 442 443 444 445 446 447
  }

  @override
  bool recommendDeferredLoading(double velocity, ScrollMetrics metrics, BuildContext context) {
    return recommendDeferredLoadingValue;
  }
}