Unverified Commit e3d9ecae authored by Jonah Williams's avatar Jonah Williams Committed by GitHub

Add ability to cache images by size (#18135)

parent dac2ebf0
...@@ -5,6 +5,7 @@ ...@@ -5,6 +5,7 @@
import 'image_stream.dart'; import 'image_stream.dart';
const int _kDefaultSize = 1000; const int _kDefaultSize = 1000;
const int _kDefaultSizeBytes = 10485760; // 10 MiB
/// Class for the [imageCache] object. /// Class for the [imageCache] object.
/// ///
...@@ -25,7 +26,8 @@ const int _kDefaultSize = 1000; ...@@ -25,7 +26,8 @@ const int _kDefaultSize = 1000;
/// Generally this class is not used directly. The [ImageProvider] class and its /// Generally this class is not used directly. The [ImageProvider] class and its
/// subclasses automatically handle the caching of images. /// subclasses automatically handle the caching of images.
class ImageCache { class ImageCache {
final Map<Object, ImageStreamCompleter> _cache = <Object, ImageStreamCompleter>{}; final Map<Object, ImageStreamCompleter> _pendingImages = <Object, ImageStreamCompleter>{};
final Map<Object, _CachedImage> _cache = <Object, _CachedImage>{};
/// Maximum number of entries to store in the cache. /// Maximum number of entries to store in the cache.
/// ///
...@@ -47,20 +49,76 @@ class ImageCache { ...@@ -47,20 +49,76 @@ class ImageCache {
_maximumSize = value; _maximumSize = value;
if (maximumSize == 0) { if (maximumSize == 0) {
_cache.clear(); _cache.clear();
_currentSizeBytes = 0;
} else { } else {
while (_cache.length > maximumSize) _checkCacheSize();
_cache.remove(_cache.keys.first);
} }
} }
/// The current number of cached entries.
int get currentSize => _cache.length;
/// Maximum size of entries to store in the cache in bytes.
///
/// Once more than this amount of bytes have been cached, the
/// least-recently-used entry is evicted until there are fewer than the
/// maximum bytes.
int get maximumSizeBytes => _maximumSizeBytes;
int _maximumSizeBytes = _kDefaultSizeBytes;
/// Changes the maximum cache bytes.
///
/// If the new size is smaller than the current size in bytes, the
/// extraneous elements are evicted immediately. Setting this to zero and then
/// returning it to its original value will therefore immediately clear the
/// cache.
set maximumSizeBytes(int value) {
assert(value != null);
assert(value >= 0);
if (value == _maximumSizeBytes)
return;
_maximumSizeBytes = value;
if (_maximumSizeBytes == 0) {
_cache.clear();
_currentSizeBytes = 0;
} else {
_checkCacheSize();
}
}
/// The current size of cached entries in bytes.
int get currentSizeBytes => _currentSizeBytes;
int _currentSizeBytes = 0;
/// Evicts all entries from the cache. /// Evicts all entries from the cache.
/// ///
/// This is useful if, for instance, the root asset bundle has been updated /// This is useful if, for instance, the root asset bundle has been updated
/// and therefore new images must be obtained. /// and therefore new images must be obtained.
// TODO(ianh): Provide a way to target individual images. This is currently non-trivial ///
// because by the time we get to the imageCache, the keys we're using are opaque. /// Images which have not finished loading yet will not be removed from the
/// cache, and when they complete they will be inserted as normal.
void clear() { void clear() {
_cache.clear(); _cache.clear();
_currentSizeBytes = 0;
}
/// Evicts a single entry from the cache, returning true if successful.
///
/// The [key] must be equal to an object used to cache an image in
/// [ImageCache.putIfAbsent].
///
/// If the key is not immediately available, as is common, consider using
/// [ImageProvider.evict] to call this method indirectly instead.
///
/// See also:
///
/// * [ImageProvider], for providing images to the [Image] widget.
bool evict(Object key) {
final _CachedImage image = _cache.remove(key);
if (image != null) {
_currentSizeBytes -= image.sizeBytes;
return true;
}
return false;
} }
/// Returns the previously cached [ImageStream] for the given key, if available; /// Returns the previously cached [ImageStream] for the given key, if available;
...@@ -71,21 +129,53 @@ class ImageCache { ...@@ -71,21 +129,53 @@ class ImageCache {
ImageStreamCompleter putIfAbsent(Object key, ImageStreamCompleter loader()) { ImageStreamCompleter putIfAbsent(Object key, ImageStreamCompleter loader()) {
assert(key != null); assert(key != null);
assert(loader != null); assert(loader != null);
ImageStreamCompleter result = _cache[key]; ImageStreamCompleter result = _pendingImages[key];
if (result != null) { // Nothing needs to be done because the image hasn't loaded yet.
// Remove the provider from the list so that we can put it back in below if (result != null)
// and thus move it to the end of the list. return result;
_cache.remove(key); // Remove the provider from the list so that we can move it to the
} else { // recently used position below.
if (_cache.length == maximumSize && maximumSize > 0) final _CachedImage image = _cache.remove(key);
_cache.remove(_cache.keys.first); if (image != null) {
result = loader(); _cache[key] = image;
return image.completer;
} }
if (maximumSize > 0) { result = loader();
assert(_cache.length < maximumSize); void listener(ImageInfo info, bool syncCall) {
_cache[key] = result; // Images that fail to load don't contribute to cache size.
final int imageSize = info.image == null ? 0 : info.image.height * info.image.width * 4;
final _CachedImage image = new _CachedImage(result, imageSize);
_currentSizeBytes += imageSize;
_pendingImages.remove(key);
_cache[key] = image;
result.removeListener(listener);
_checkCacheSize();
}
if (maximumSize > 0 && maximumSizeBytes > 0) {
_pendingImages[key] = result;
result.addListener(listener);
} }
assert(_cache.length <= maximumSize);
return result; return result;
} }
// Remove images from the cache until both the length and bytes are below
// maximum, or the cache is empty.
void _checkCacheSize() {
while (_currentSizeBytes > _maximumSizeBytes || _cache.length > _maximumSize) {
final Object key = _cache.keys.first;
final _CachedImage image = _cache[key];
_currentSizeBytes -= image.sizeBytes;
_cache.remove(key);
}
assert(_currentSizeBytes >= 0);
assert(_cache.length <= maximumSize);
assert(_currentSizeBytes <= maximumSizeBytes);
}
}
class _CachedImage {
_CachedImage(this.completer, this.sizeBytes);
final ImageStreamCompleter completer;
final int sizeBytes;
} }
...@@ -12,6 +12,7 @@ import 'package:flutter/foundation.dart'; ...@@ -12,6 +12,7 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'binding.dart'; import 'binding.dart';
import 'image_cache.dart';
import 'image_stream.dart'; import 'image_stream.dart';
/// Configuration information passed to the [ImageProvider.resolve] method to /// Configuration information passed to the [ImageProvider.resolve] method to
...@@ -284,6 +285,49 @@ abstract class ImageProvider<T> { ...@@ -284,6 +285,49 @@ abstract class ImageProvider<T> {
return stream; return stream;
} }
/// Evicts an entry from the image cache.
///
/// Returns a [Future] which indicates whether the value was successfully
/// removed.
///
/// The [ImageProvider] used does not need to be the same instance that was
/// passed to an [Image] widget, but it does need to create a key which is
/// equal to one.
///
/// The [cache] is optional and defaults to the global image cache.
///
/// The [configuration] is optional and defaults to
/// [ImageConfiguration.empty].
///
/// ## Sample code
///
/// The following sample code shows how an image loaded using the [Image]
/// widget can be evicted using a [NetworkImage] with a matching url.
///
/// ```dart
/// class MyWidget extends StatelessWidget {
/// final String url = '...';
///
/// @override
/// Widget build(BuildContext context) {
/// return new Image.network(url);
/// }
///
/// void evictImage() {
/// final NetworkImage provider = new NetworkImage(url);
/// provider.evict().then<void>((bool success) {
/// if (success)
/// debugPrint('removed image!');
/// });
/// }
/// }
/// ```
Future<bool> evict({ImageCache cache, ImageConfiguration configuration = ImageConfiguration.empty}) async {
cache ??= imageCache;
final T key = await obtainKey(configuration);
return cache.evict(key);
}
/// Converts an ImageProvider's settings plus an ImageConfiguration to a key /// Converts an ImageProvider's settings plus an ImageConfiguration to a key
/// that describes the precise image to load. /// that describes the precise image to load.
/// ///
......
...@@ -10,35 +10,70 @@ import 'mocks_for_image_cache.dart'; ...@@ -10,35 +10,70 @@ import 'mocks_for_image_cache.dart';
void main() { void main() {
new TestRenderingFlutterBinding(); // initializes the imageCache new TestRenderingFlutterBinding(); // initializes the imageCache
group(ImageCache, () {
tearDown(() {
imageCache.clear();
imageCache.maximumSize = 1000;
imageCache.maximumSizeBytes = 10485760;
});
test('Image cache resizing', () async { test('Image cache resizing based on count', () async {
imageCache.maximumSize = 2;
imageCache.maximumSize = 2; final TestImageInfo a = await extractOneFrame(const TestImageProvider(1, 1).resolve(ImageConfiguration.empty));
final TestImageInfo b = await extractOneFrame(const TestImageProvider(2, 2).resolve(ImageConfiguration.empty));
final TestImageInfo c = await extractOneFrame(const TestImageProvider(3, 3).resolve(ImageConfiguration.empty));
final TestImageInfo d = await extractOneFrame(const TestImageProvider(1, 4).resolve(ImageConfiguration.empty));
expect(a.value, equals(1));
expect(b.value, equals(2));
expect(c.value, equals(3));
expect(d.value, equals(4));
final TestImageInfo a = await extractOneFrame(const TestImageProvider(1, 1).resolve(ImageConfiguration.empty)); imageCache.maximumSize = 0;
final TestImageInfo b = await extractOneFrame(const TestImageProvider(2, 2).resolve(ImageConfiguration.empty));
final TestImageInfo c = await extractOneFrame(const TestImageProvider(3, 3).resolve(ImageConfiguration.empty));
final TestImageInfo d = await extractOneFrame(const TestImageProvider(1, 4).resolve(ImageConfiguration.empty));
expect(a.value, equals(1));
expect(b.value, equals(2));
expect(c.value, equals(3));
expect(d.value, equals(4));
imageCache.maximumSize = 0; final TestImageInfo e = await extractOneFrame(const TestImageProvider(1, 5).resolve(ImageConfiguration.empty));
expect(e.value, equals(5));
final TestImageInfo e = await extractOneFrame(const TestImageProvider(1, 5).resolve(ImageConfiguration.empty)); final TestImageInfo f = await extractOneFrame(const TestImageProvider(1, 6).resolve(ImageConfiguration.empty));
expect(e.value, equals(5)); expect(f.value, equals(6));
final TestImageInfo f = await extractOneFrame(const TestImageProvider(1, 6).resolve(ImageConfiguration.empty)); imageCache.maximumSize = 3;
expect(f.value, equals(6));
imageCache.maximumSize = 3; final TestImageInfo g = await extractOneFrame(const TestImageProvider(1, 7).resolve(ImageConfiguration.empty));
expect(g.value, equals(7));
final TestImageInfo g = await extractOneFrame(const TestImageProvider(1, 7).resolve(ImageConfiguration.empty)); final TestImageInfo h = await extractOneFrame(const TestImageProvider(1, 8).resolve(ImageConfiguration.empty));
expect(g.value, equals(7)); expect(h.value, equals(7));
});
final TestImageInfo h = await extractOneFrame(const TestImageProvider(1, 8).resolve(ImageConfiguration.empty)); test('Image cache resizing based on size', () async {
expect(h.value, equals(7)); const TestImage testImage = const TestImage(width: 8, height: 8); // 256 B.
imageCache.maximumSizeBytes = 256 * 2;
final TestImageInfo a = await extractOneFrame(const TestImageProvider(1, 1, image: testImage).resolve(ImageConfiguration.empty));
final TestImageInfo b = await extractOneFrame(const TestImageProvider(2, 2, image: testImage).resolve(ImageConfiguration.empty));
final TestImageInfo c = await extractOneFrame(const TestImageProvider(3, 3, image: testImage).resolve(ImageConfiguration.empty));
final TestImageInfo d = await extractOneFrame(const TestImageProvider(1, 4, image: testImage).resolve(ImageConfiguration.empty));
expect(a.value, equals(1));
expect(b.value, equals(2));
expect(c.value, equals(3));
expect(d.value, equals(4));
imageCache.maximumSizeBytes = 0;
final TestImageInfo e = await extractOneFrame(const TestImageProvider(1, 5, image: testImage).resolve(ImageConfiguration.empty));
expect(e.value, equals(5));
final TestImageInfo f = await extractOneFrame(const TestImageProvider(1, 6, image: testImage).resolve(ImageConfiguration.empty));
expect(f.value, equals(6));
imageCache.maximumSizeBytes = 256 * 3;
final TestImageInfo g = await extractOneFrame(const TestImageProvider(1, 7, image: testImage).resolve(ImageConfiguration.empty));
expect(g.value, equals(7));
final TestImageInfo h = await extractOneFrame(const TestImageProvider(1, 8, image: testImage).resolve(ImageConfiguration.empty));
expect(h.value, equals(7));
});
}); });
} }
...@@ -10,77 +10,113 @@ import 'mocks_for_image_cache.dart'; ...@@ -10,77 +10,113 @@ import 'mocks_for_image_cache.dart';
void main() { void main() {
new TestRenderingFlutterBinding(); // initializes the imageCache new TestRenderingFlutterBinding(); // initializes the imageCache
group(ImageCache, () {
tearDown(() {
imageCache.clear();
imageCache.maximumSize = 1000;
imageCache.maximumSizeBytes = 10485760;
});
test('Image cache', () async { test('maintains cache size', () async {
imageCache.maximumSize = 3;
imageCache.maximumSize = 3; final TestImageInfo a = await extractOneFrame(const TestImageProvider(1, 1).resolve(ImageConfiguration.empty));
expect(a.value, equals(1));
final TestImageInfo b = await extractOneFrame(const TestImageProvider(1, 2).resolve(ImageConfiguration.empty));
expect(b.value, equals(1));
final TestImageInfo c = await extractOneFrame(const TestImageProvider(1, 3).resolve(ImageConfiguration.empty));
expect(c.value, equals(1));
final TestImageInfo d = await extractOneFrame(const TestImageProvider(1, 4).resolve(ImageConfiguration.empty));
expect(d.value, equals(1));
final TestImageInfo e = await extractOneFrame(const TestImageProvider(1, 5).resolve(ImageConfiguration.empty));
expect(e.value, equals(1));
final TestImageInfo f = await extractOneFrame(const TestImageProvider(1, 6).resolve(ImageConfiguration.empty));
expect(f.value, equals(1));
final TestImageInfo a = await extractOneFrame(const TestImageProvider(1, 1).resolve(ImageConfiguration.empty)); expect(f, equals(a));
expect(a.value, equals(1));
final TestImageInfo b = await extractOneFrame(const TestImageProvider(1, 2).resolve(ImageConfiguration.empty));
expect(b.value, equals(1));
final TestImageInfo c = await extractOneFrame(const TestImageProvider(1, 3).resolve(ImageConfiguration.empty));
expect(c.value, equals(1));
final TestImageInfo d = await extractOneFrame(const TestImageProvider(1, 4).resolve(ImageConfiguration.empty));
expect(d.value, equals(1));
final TestImageInfo e = await extractOneFrame(const TestImageProvider(1, 5).resolve(ImageConfiguration.empty));
expect(e.value, equals(1));
final TestImageInfo f = await extractOneFrame(const TestImageProvider(1, 6).resolve(ImageConfiguration.empty));
expect(f.value, equals(1));
expect(f, equals(a)); // cache still only has one entry in it: 1(1)
// cache still only has one entry in it: 1(1) final TestImageInfo g = await extractOneFrame(const TestImageProvider(2, 7).resolve(ImageConfiguration.empty));
expect(g.value, equals(7));
final TestImageInfo g = await extractOneFrame(const TestImageProvider(2, 7).resolve(ImageConfiguration.empty)); // cache has two entries in it: 1(1), 2(7)
expect(g.value, equals(7));
// cache has two entries in it: 1(1), 2(7) final TestImageInfo h = await extractOneFrame(const TestImageProvider(1, 8).resolve(ImageConfiguration.empty));
expect(h.value, equals(1));
final TestImageInfo h = await extractOneFrame(const TestImageProvider(1, 8).resolve(ImageConfiguration.empty)); // cache still has two entries in it: 2(7), 1(1)
expect(h.value, equals(1));
// cache still has two entries in it: 2(7), 1(1) final TestImageInfo i = await extractOneFrame(const TestImageProvider(3, 9).resolve(ImageConfiguration.empty));
expect(i.value, equals(9));
final TestImageInfo i = await extractOneFrame(const TestImageProvider(3, 9).resolve(ImageConfiguration.empty)); // cache has three entries in it: 2(7), 1(1), 3(9)
expect(i.value, equals(9));
// cache has three entries in it: 2(7), 1(1), 3(9) final TestImageInfo j = await extractOneFrame(const TestImageProvider(1, 10).resolve(ImageConfiguration.empty));
expect(j.value, equals(1));
final TestImageInfo j = await extractOneFrame(const TestImageProvider(1, 10).resolve(ImageConfiguration.empty)); // cache still has three entries in it: 2(7), 3(9), 1(1)
expect(j.value, equals(1));
// cache still has three entries in it: 2(7), 3(9), 1(1) final TestImageInfo k = await extractOneFrame(const TestImageProvider(4, 11).resolve(ImageConfiguration.empty));
expect(k.value, equals(11));
final TestImageInfo k = await extractOneFrame(const TestImageProvider(4, 11).resolve(ImageConfiguration.empty)); // cache has three entries: 3(9), 1(1), 4(11)
expect(k.value, equals(11));
// cache has three entries: 3(9), 1(1), 4(11) final TestImageInfo l = await extractOneFrame(const TestImageProvider(1, 12).resolve(ImageConfiguration.empty));
expect(l.value, equals(1));
final TestImageInfo l = await extractOneFrame(const TestImageProvider(1, 12).resolve(ImageConfiguration.empty)); // cache has three entries: 3(9), 4(11), 1(1)
expect(l.value, equals(1));
// cache has three entries: 3(9), 4(11), 1(1) final TestImageInfo m = await extractOneFrame(const TestImageProvider(2, 13).resolve(ImageConfiguration.empty));
expect(m.value, equals(13));
final TestImageInfo m = await extractOneFrame(const TestImageProvider(2, 13).resolve(ImageConfiguration.empty)); // cache has three entries: 4(11), 1(1), 2(13)
expect(m.value, equals(13));
// cache has three entries: 4(11), 1(1), 2(13) final TestImageInfo n = await extractOneFrame(const TestImageProvider(3, 14).resolve(ImageConfiguration.empty));
expect(n.value, equals(14));
final TestImageInfo n = await extractOneFrame(const TestImageProvider(3, 14).resolve(ImageConfiguration.empty)); // cache has three entries: 1(1), 2(13), 3(14)
expect(n.value, equals(14));
// cache has three entries: 1(1), 2(13), 3(14) final TestImageInfo o = await extractOneFrame(const TestImageProvider(4, 15).resolve(ImageConfiguration.empty));
expect(o.value, equals(15));
final TestImageInfo o = await extractOneFrame(const TestImageProvider(4, 15).resolve(ImageConfiguration.empty)); // cache has three entries: 2(13), 3(14), 4(15)
expect(o.value, equals(15));
// cache has three entries: 2(13), 3(14), 4(15) final TestImageInfo p = await extractOneFrame(const TestImageProvider(1, 16).resolve(ImageConfiguration.empty));
expect(p.value, equals(16));
final TestImageInfo p = await extractOneFrame(const TestImageProvider(1, 16).resolve(ImageConfiguration.empty)); // cache has three entries: 3(14), 4(15), 1(16)
expect(p.value, equals(16));
// cache has three entries: 3(14), 4(15), 1(16) });
test('clear removes all images and resets cache size', () async {
const TestImage testImage = const TestImage(width: 8, height: 8);
expect(imageCache.currentSize, 0);
expect(imageCache.currentSizeBytes, 0);
await extractOneFrame(const TestImageProvider(1, 1, image: testImage).resolve(ImageConfiguration.empty));
await extractOneFrame(const TestImageProvider(2, 2, image: testImage).resolve(ImageConfiguration.empty));
expect(imageCache.currentSize, 2);
expect(imageCache.currentSizeBytes, 256 * 2);
imageCache.clear();
expect(imageCache.currentSize, 0);
expect(imageCache.currentSizeBytes, 0);
});
test('evicts individual images', () async {
const TestImage testImage = const TestImage(width: 8, height: 8);
await extractOneFrame(const TestImageProvider(1, 1, image: testImage).resolve(ImageConfiguration.empty));
await extractOneFrame(const TestImageProvider(2, 2, image: testImage).resolve(ImageConfiguration.empty));
expect(imageCache.currentSize, 2);
expect(imageCache.currentSizeBytes, 256 * 2);
expect(imageCache.evict(1), true);
expect(imageCache.currentSize, 1);
expect(imageCache.currentSizeBytes, 256);
});
}); });
} }
...@@ -2,13 +2,56 @@ ...@@ -2,13 +2,56 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
import 'dart:async';
import 'dart:typed_data';
import 'package:flutter/painting.dart'; import 'package:flutter/painting.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import '../rendering/rendering_tester.dart';
import 'image_data.dart';
void main() { void main() {
test('NetworkImage non-null url test', () { new TestRenderingFlutterBinding(); // initializes the imageCache
expect(() { group(ImageProvider, () {
new NetworkImage(nonconst(null)); tearDown(() {
}, throwsAssertionError); imageCache.clear();
});
test('NetworkImage non-null url test', () {
expect(() {
new NetworkImage(nonconst(null));
}, throwsAssertionError);
});
test('ImageProvider can evict images', () async {
final Uint8List bytes = new Uint8List.fromList(kTransparentImage);
final MemoryImage imageProvider = new MemoryImage(bytes);
final ImageStream stream = imageProvider.resolve(ImageConfiguration.empty);
final Completer<void> completer = new Completer<void>();
stream.addListener((ImageInfo info, bool syncCall) => completer.complete());
await completer.future;
expect(imageCache.currentSize, 1);
expect(await new MemoryImage(bytes).evict(), true);
expect(imageCache.currentSize, 0);
});
test('ImageProvider.evict respects the provided ImageCache', () async {
final ImageCache otherCache = new ImageCache();
final Uint8List bytes = new Uint8List.fromList(kTransparentImage);
final MemoryImage imageProvider = new MemoryImage(bytes);
otherCache.putIfAbsent(imageProvider, () => imageProvider.load(imageProvider));
final ImageStream stream = imageProvider.resolve(ImageConfiguration.empty);
final Completer<void> completer = new Completer<void>();
stream.addListener((ImageInfo info, bool syncCall) => completer.complete());
await completer.future;
expect(otherCache.currentSize, 1);
expect(imageCache.currentSize, 1);
expect(await imageProvider.evict(cache: otherCache), true);
expect(otherCache.currentSize, 0);
expect(imageCache.currentSize, 1);
});
}); });
} }
...@@ -3,7 +3,9 @@ ...@@ -3,7 +3,9 @@
// found in the LICENSE file. // found in the LICENSE file.
import 'dart:async'; import 'dart:async';
import 'dart:typed_data';
import 'dart:ui' as ui show Image; import 'dart:ui' as ui show Image;
import 'dart:ui';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/painting.dart'; import 'package:flutter/painting.dart';
...@@ -54,3 +56,19 @@ Future<ImageInfo> extractOneFrame(ImageStream stream) { ...@@ -54,3 +56,19 @@ Future<ImageInfo> extractOneFrame(ImageStream stream) {
stream.addListener(listener); stream.addListener(listener);
return completer.future; return completer.future;
} }
class TestImage implements ui.Image {
const TestImage({this.height = 0, this.width = 0});
@override
final int height;
@override
final int width;
@override
void dispose() {}
@override
Future<ByteData> toByteData({ImageByteFormat format = ImageByteFormat.rawRgba}) {
throw new UnimplementedError();
}
}
...@@ -295,7 +295,7 @@ void main() { ...@@ -295,7 +295,7 @@ void main() {
final TestImageProvider imageProvider = new TestImageProvider(); final TestImageProvider imageProvider = new TestImageProvider();
await tester.pumpWidget(new Image(image: imageProvider)); await tester.pumpWidget(new Image(image: imageProvider));
final State<Image> image = tester.state/*State<Image>*/(find.byType(Image)); final State<Image> image = tester.state/*State<Image>*/(find.byType(Image));
expect(image.toString(), equalsIgnoringHashCodes('_ImageState#00000(stream: ImageStream#00000(OneFrameImageStreamCompleter#00000, unresolved, 1 listener), pixels: null)')); expect(image.toString(), equalsIgnoringHashCodes('_ImageState#00000(stream: ImageStream#00000(OneFrameImageStreamCompleter#00000, unresolved, 2 listeners), pixels: null)'));
imageProvider.complete(); imageProvider.complete();
await tester.pump(); await tester.pump();
expect(image.toString(), equalsIgnoringHashCodes('_ImageState#00000(stream: ImageStream#00000(OneFrameImageStreamCompleter#00000, [100×100] @ 1.0x, 1 listener), pixels: [100×100] @ 1.0x)')); expect(image.toString(), equalsIgnoringHashCodes('_ImageState#00000(stream: ImageStream#00000(OneFrameImageStreamCompleter#00000, [100×100] @ 1.0x, 1 listener), pixels: [100×100] @ 1.0x)'));
...@@ -353,14 +353,14 @@ void main() { ...@@ -353,14 +353,14 @@ void main() {
child: image, child: image,
), ),
); );
expect(imageStreamCompleter.listeners.length, 1); expect(imageStreamCompleter.listeners.length, 2);
await tester.pumpWidget( await tester.pumpWidget(
new TickerMode( new TickerMode(
enabled: false, enabled: false,
child: image, child: image,
), ),
); );
expect(imageStreamCompleter.listeners.length, 0); expect(imageStreamCompleter.listeners.length, 1);
}); });
testWidgets('Verify Image shows correct RenderImage when changing to an already completed provider', (WidgetTester tester) async { testWidgets('Verify Image shows correct RenderImage when changing to an already completed provider', (WidgetTester tester) async {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment