Optimizing Image Size Retrieval and Memory Management in Flutter
This article examines a Flutter technique for obtaining image dimensions, identifies missing listener removal that can cause memory leaks, and presents an enhanced extension using ImageProvider and ImageDescriptor to safely retrieve size information with optional decode avoidance.
Optimizing Image Size Retrieval and Memory Management in Flutter
The article starts from a popular Flutter snippet that returns an image's aspect ratio, then analyses why the original implementation can lead to memory leaks and runtime errors.
Problems in the original code
The original extension GetImageAspectRatio on Image adds an ImageStreamListener but never removes it. When the listener is not removed, the ImageStreamCompleter stays in ImageCache._liveImages , keeping the decoded image data in memory and potentially causing OOM, especially for large or animated images.
Additionally, the listener may be added multiple times, and the Completer.complete call can be invoked more than once, throwing an exception.
import 'dart:async' show Completer;
import 'package:flutter/material.dart' show Image, ImageConfiguration, ImageStreamListener;
extension GetImageAspectRatio on Image {
Future
getAspectRatio() {
final completer = Completer
();
image.resolve(const ImageConfiguration()).addListener(
ImageStreamListener((imageInfo, synchronousCall) {
final aspectRatio = imageInfo.image.width / imageInfo.image.height;
imageInfo.image.dispose();
completer.complete(aspectRatio);
}),
);
return completer.future;
}
}Improved implementation
The fix moves the extension to ImageProvider , adds proper removeListener calls for both success and error paths, and returns a Size object instead of a raw ratio.
extension GetImageAspectRatio on ImageProvider {
Future
getImageSize() {
final completer = Completer
();
ImageStream imageStream = resolve(const ImageConfiguration());
ImageStreamListener? listener;
listener = ImageStreamListener(
(imageInfo, synchronousCall) {
if (!completer.isCompleted) {
completer.complete(Size(imageInfo.image.width.toDouble(), imageInfo.image.height.toDouble()));
}
WidgetsBinding.instance.addPostFrameCallback((_){
imageInfo.dispose();
imageStream.removeListener(listener!);
});
},
onError: (exception, stackTrace) {
if (!completer.isCompleted) completer.complete();
imageStream.removeListener(listener!);
},
);
imageStream.addListener(listener);
return completer.future;
}
}Zero‑decode option with ImageDescriptor
For local or in‑memory images, the article shows how to avoid full decoding by using ImageDescriptor.encoded on an ImmutableBuffer . This extracts width and height directly from the image header.
extension GetImageSize on ImageProvider {
Future
getImageSize({bool avoidDecode = false}) async {
if (avoidDecode) {
final cacheStatus = await obtainCacheStatus(configuration: const ImageConfiguration());
final tracked = cacheStatus?.tracked ?? false;
if (!tracked) {
ImmutableBuffer? buffer;
if (this is AssetBundleImageProvider) {
final key = await obtainKey(const ImageConfiguration()) as AssetBundleImageKey;
buffer = await key.bundle.loadBuffer(key.name);
} else if (this is FileImage) {
final file = (this as FileImage).file;
final length = await file.length();
if (length > 0) buffer = await ImmutableBuffer.fromFilePath(file.path);
} else if (this is MemoryImage) {
final bytes = (this as MemoryImage).bytes;
buffer = await ImmutableBuffer.fromUint8List(bytes);
}
if (buffer != null) {
final descriptor = await ImageDescriptor.encoded(buffer);
final size = Size(descriptor.width.toDouble(), descriptor.height.toDouble());
buffer.dispose();
descriptor.dispose();
if (!size.isEmpty) return size;
}
}
}
// Fallback to normal listener‑based approach
final completer = Completer
();
ImageStream imageStream = resolve(const ImageConfiguration());
ImageStreamListener? listener;
listener = ImageStreamListener(
(imageInfo, synchronousCall) {
if (!completer.isCompleted) {
completer.complete(Size(imageInfo.image.width.toDouble(), imageInfo.image.height.toDouble()));
}
WidgetsBinding.instance.addPostFrameCallback((_){
imageInfo.dispose();
imageStream.removeListener(listener!);
});
},
onError: (e, s) {
if (!completer.isCompleted) completer.complete();
imageStream.removeListener(listener!);
},
);
imageStream.addListener(listener);
return completer.future;
}
}Usage examples
void main() {
// Asset image
const AssetImage('assets/images/4k.jpg')
.getImageSize(avoidDecode: true)
.then((s) => print('the size is $s'));
// File image
final file = File('the/disk/image/path');
FileImage(file).getImageSize(avoidDecode: true).then((s) => print('the size is $s'));
// Memory image
Uint8List data = Uint8List.fromList([/* ... */]);
MemoryImage(data).getImageSize(avoidDecode: true).then((s) => print('the size is $s'));
// Cached network image (decoded and cached)
CachedNetworkImageProvider('https://example.com/img.png')
.getImageSize()
.then((s) => print('the size is $s'));
}Overall, the article emphasizes careful listener management, proper disposal of image resources, and provides a flexible utility that works for asset, file, memory, and network images while optionally avoiding costly decoding.
Rare Earth Juejin Tech Community
Juejin, a tech community that helps developers grow.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.