Open Klabauterman opened 2 years ago
I tried to use ImageProvider
before but I found some issues especially with memory usage, that why I am using Image
directly from dart: ui
.
I also think the package should use ImageProvider
because it is the standard and many widgets depend on it but it will take some time to get the same functionality due to the restrictions that we have on ImageProvider
class.
@7mada123 I think I know what you mean. I am trying to understand the framework around ImageProvider
ImageStream
and ImageStreamCompleter
.
I started playing around by copying the code from painting/_network_image_io.dart
and changed the loading code. The problem seems to be that an ImageStreamCompleter
which handles the loading is disposed as soon as there are no more listeners on it. That would be the point to cancel a network request. But one of the listeners is the ImageCache, so there is still one listener left, when the actual image widget gets disposed and we cannot cancel the request.
If I find a nice way around that, I'll let you know.
In case you are interested, I figured it out, how it works with the ImageProviders. It is kind of hacky, but it works.
We need to implement a custom ImageStreamCompleter
which is returned by our custom ImageProvider
that keeps track of its listeners and its loading state. The flutter framework uses an ImageCache
as a singleton. It keeps track of currently loading images and caches loaded images in memory. We need to take into account that the first listener of ImageStreamCompleter
is always the ImageCache, so if there is only 1 listener left during the loading of the image. It is not used anymore and we can cancel the request.
The completer could look like this:
class DisposableImageCompleter extends MultiFrameImageStreamCompleter {
final Object key;
DisposableImageCompleter({
required this.key,
required Future<ui.Codec> codec,
required double scale,
String? debugLabel,
Stream<ImageChunkEvent>? chunkEvents,
InformationCollector? informationCollector,
}) : super(
codec: codec,
scale: scale,
debugLabel: debugLabel,
chunkEvents: chunkEvents,
informationCollector: informationCollector);
int listenerCount = 0;
bool loaded = false;
@override
void setImage(ImageInfo image) {
loaded = true;
super.setImage(image);
}
@override
void addListener(ImageStreamListener listener) {
//print("ADD LISTENER");
super.addListener(listener);
listenerCount++;
}
@override
void removeListener(ImageStreamListener listener) {
//print("REMOVE LISTENER");
super.removeListener(listener);
listenerCount--;
if (listenerCount == 1 && !loaded) {
// ONLY ImageCache is subscribed -> remove it if it has not finished
PaintingBinding.instance.imageCache.evict(key);
}
}
@override
void reportError(
{DiagnosticsNode? context,
required Object exception,
StackTrace? stack,
InformationCollector? informationCollector,
bool silent = false}) {
//DO NOT REPORT ANYTHING WHEN THE IMAGE LOAD FAILED OR WAS CANCELLED
}
}
In the ImageProvider we could use the event stream as an indicator when we can cancel the request.
class CachingNetworkImageProvider extends ImageProvider<NetworkImage> implements NetworkImage {
const CachingNetworkImageProvider(this.url, {this.scale = 1.0, this.headers});
@override
final String url;
@override
final double scale;
@override
final Map<String, String>? headers;
@override
Future<NetworkImage> obtainKey(ImageConfiguration configuration) {
return SynchronousFuture<NetworkImage>(this);
}
@override
bool operator ==(Object other) {
if (other.runtimeType != runtimeType) {
return false;
}
return other is NetworkImage && other.url == url && other.scale == scale;
}
@override
int get hashCode => Object.hash(url, scale);
@override
String toString() => '${objectRuntimeType(this, 'NetworkImage')}("$url", scale: $scale)';
@override
ImageStreamCompleter load(NetworkImage key, DecoderCallback decode) {
// Ownership of this controller is handed off to [_loadAsync]; it is that
// method's responsibility to close the controller's stream when the image
// has been loaded or an error is thrown.
final StreamController<ImageChunkEvent> chunkEvents = StreamController<ImageChunkEvent>();
return DisposableImageCompleter(
key: key,
codec: _loadAsync(key, chunkEvents, decode),
chunkEvents: chunkEvents.stream,
scale: key.scale,
debugLabel: key.url,
);
}
Future<ui.Codec> _loadAsync(
NetworkImage key,
StreamController<ImageChunkEvent> chunkEvents,
DecoderCallback decode,
) async {
try {
final networkLoader =
NetworkLoader(url: key.url, headers: key.headers, chunkEvents: chunkEvents);
final fileCache = FileLoader(url: key.url);
chunkEvents.onCancel = () {
networkLoader.cancel();
fileCache.cancel();
};
var bytes = await fileCache.load();
if (bytes == null) {
bytes = await networkLoader.load();
if (bytes != null) {
fileCache.store(url, bytes);
}
}
if (bytes == null) throw Exception("No image returned");
return decode(bytes);
} finally {
chunkEvents.close();
}
}
}
That's great @Klabauterman, thanks for the clarification, This may take a while to be implemented and tested, I think I'll be able to work on it next week, if you have some spare time please contribute so we can get things done faster.
Please add this feature.
I was using cached_network_image and I need to switch for my project, because it is constantly crashing on iOS. The problem with that library was that network requests are never cancelled which is a problem if the user scrolls through a long list of images.
So I started to compare implementations of Image caching libraries. The pros of this library is that it uses isolates and it cancels network requests. Compared to other libraries it comes with one great disadvantage: It does not provide ImageProviders which seems to be the flutter standard when handling images. ImageProviders also use the flutter ImageCache as a in-memory-cache which already works well. It would be really great to get the best of both worlds.
I am thinking about writing a proof of concept.