skip to Main Content

I sometime got the following error in my Crashlytics:

Bad state: Future already completed. Error thrown by an image listener.

This is my function to calculate an image dimension, where I think the error is thrown:

Future<Size> calculateImageDimension(
    String imageUrl, BuildContext context) async {
  Completer<Size> completer = Completer();
  late Image image;
  image = Image.network(
    imageUrl,
  );

  if (!await hasValidImageData(imageUrl, context)) {
    // ignore: avoid_print
    print('invaaaaliiiiiiiiiiiiiiddddd');
    return Future.value(const Size(0, 0));
  }

  image.image.resolve(const ImageConfiguration()).addListener(
    ImageStreamListener(
      (ImageInfo image, bool synchronousCall) {
        var myImage = image.image;
        Size size = Size(myImage.width.toDouble(), myImage.height.toDouble());
        if (!completer.isCompleted) {
          completer.complete(size);
        }
      },
    ),
  );

  return completer.future;
}

What am I doing wrong here? I couldn’t reproduce the error on my side… Let me know if you need any more info.

2

Answers


  1. Just a random thought, have you tried to replace

    return Future.value(const Size(0, 0));
    

    with something like

    Completer.complete(const Size(0, 0));
    return completer.future;
    

    My guess is that at some point (at the start probably), there’s no valid image data and thus it returns a completed Future. Another option is that there’s a race condition somewhere but without more information it’d be hard to point you towards the right direction.

    Login or Signup to reply.
  2. I think your Future is trying to completed more than once. I mean the Future returning by your calculateImageDimension is trying to return and complete again and again so that’s probably create the error.

    After checking your code, I see here that Future is inside your callback(ImageStreamListener) and called every time if the image is being loaded from the network and/or the network conditions change until the image is resolve.

    So, let me change the structure of your code like below;

    Future<Size> calculateImageDimension(String imageUrl, BuildContext context) async {
     Completer<Size> completer = Completer();
     ImageProvider imageProvider = NetworkImage(imageUrl);
    
     if (!await hasValidImageData(imageUrl, context)) {
       print('invaaaaliiiiiiiiiiiiiiddddd');
       return Future.value(const Size(0, 0));
     }
    
     ImageStream stream = imageProvider.resolve(const ImageConfiguration());
     ImageStreamListener listener = ImageStreamListener(
       (ImageInfo image, bool synchronousCall) {
         var myImage = image.image;
         Size size = Size(myImage.width.toDouble(), myImage.height.toDouble());
         if (!completer.isCompleted) {
           completer.complete(size);
         }
       },
     );
    
     stream.addListener(listener);
    
     return completer.future;
    

    See that the ImageStreamListener is added to the ImageStream returned by the resolve method of the ImageProvider. This because we need to ensure that your Future is only completed once when the image is loaded for the first time. For this, even if the image will loaded multiple times, it will not affect the Future and not be completed again. This approach hopingly to prevent error(Bad state: Future already completed.)

    Lastly, when your done, do call removeListener method on the ImageStream to remove the listener by a stream.removeListener(listener);. This should be done in the dispose method of your widget, or wherever you’re done with the Future returned by calculateImageDimension.

    This seems to be unnecessary, but we use it to prevent memory leaks.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search