Mobile Development 16 min read

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.

Rare Earth Juejin Tech Community
Rare Earth Juejin Tech Community
Rare Earth Juejin Tech Community
Optimizing Image Size Retrieval and Memory Management in Flutter

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.

DartFluttermobile developmentMemory LeakImageaspect-ratioimageprovider
Rare Earth Juejin Tech Community
Written by

Rare Earth Juejin Tech Community

Juejin, a tech community that helps developers grow.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.