Flutter ImageStream Bad state: Future already completed

1,662

If the ImageStream emits more than once you will call completer.complete() twice, which is an error. As per the ImageStream documentation this can happen if the image is animating, or if the resource is changed.

If you only care about the first emit you can await image.image.resolve(ImageConfiguration()).first. A more hacky solution would be to call complete() only if completer.isCompleted == false.

Share:
1,662
Chris
Author by

Chris

Updated on January 04, 2023

Comments

  • Chris
    Chris over 1 year

    I have an WebView inside my app where I crawl the current Website.

    This is the procedure:

    1. User taps on button
    2. crawl the content of the current URL
    3. get all the images
    4. for each image get its dimension
    5. print out first three elements of sorted List

    The Problem is 4:

    This is my code for it:

    Future<Size> _calculateImageDimension(String imageUrl) {
      Completer<Size> completer = Completer();
      Image image = Image.network(imageUrl);
      image.image.resolve(ImageConfiguration()).addListener(
        ImageStreamListener(
          (ImageInfo image, bool synchronousCall) {
            var myImage = image.image;
            Size size = Size(myImage.width.toDouble(), myImage.height.toDouble());
            completer.complete(size);  // <- StateError
          },
        ),
      );
    
      return completer.future;
    }
    

    This fails with:

    Bad state: Future already completed

    Now the weird part is that it only fails on some URL's.

    What is wrong with my _calculateImageDimension? What am I missing?

    This is the complete Code:

    import 'package:boilerplate/ui/shared_widgets/buttons/rounded_corner_text_button.dart';
    import 'package:flutter/material.dart';
    import 'package:get/get.dart';
    import 'package:webview_flutter/webview_flutter.dart';
    import 'dart:async';
    import 'package:http/http.dart' as http;
    import 'package:html/parser.dart' as parser;
    import 'package:html/dom.dart' as dom;
    
    class WebViewExample extends StatefulWidget {
      @override
      _WebViewExampleState createState() => _WebViewExampleState();
    }
    
    class _WebViewExampleState extends State<WebViewExample> {
    // Reference to webview controller
      late WebViewController _controller;
    
      final _stopwatch = Stopwatch();
      String _currentUrl = '';
      List<ImageWithSize> _imagesWithSize = [];
    
      bool _isLoading = true;
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(
            title: Text('Flutter Web View Example'),
          ),
          body: SafeArea(
            child: Column(
              children: [
                Expanded(
                  child: WebView(
                    initialUrl:
                        'https://www.prettylittlething.com/recycled-green-towelling-oversized-beach-shirt.html',
                    javascriptMode: JavascriptMode.unrestricted,
                    onWebViewCreated: (WebViewController webViewController) {
                      // Get reference to WebView controller to access it globally
                      _controller = webViewController;
                    },
                    javascriptChannels: <JavascriptChannel>{
                      // Set Javascript Channel to WebView
                      _extractDataJSChannel(context),
                    },
                    onPageStarted: (String url) {
                      setState(() {
                        _isLoading = true;
                      });
                    },
                    onPageFinished: (String url) {
                      setState(() {
                        _imagesWithSize = [];
    
                        _currentUrl = url;
                        _isLoading = false;
                      });
                    },
                  ),
                ),
                RoundedCornersTextButton(
                    title: 'GET',
                    isEnabled: !_isLoading,
                    onTap: () {
                      _getData();
                    }),
              ],
            ),
          ),
        );
      }
    
      JavascriptChannel _extractDataJSChannel(BuildContext context) {
        return JavascriptChannel(
          name: 'Flutter',
          onMessageReceived: (JavascriptMessage message) {
            String pageBody = message.message;
          },
        );
      }
    
      void _getData() async {
        // print(url);
        _stopwatch.start();
        final response = await http.get(Uri.parse(_currentUrl));
        final host = Uri.parse(_currentUrl).host;
        dom.Document document = parser.parse(response.body);
        final elements = document.getElementsByTagName("img").toList();
        for (var element in elements) {
          var imageSource = element.attributes['src'] ?? '';
    
          bool validURL = Uri.parse(imageSource).host == '' ||
                  Uri.parse(host + imageSource).host == ''
              ? false
              : true;
    
          if (validURL && !imageSource.endsWith('svg')) {
            Uri imageSourceUrl = Uri.parse(imageSource);
            if (imageSourceUrl.host.isEmpty) {
              imageSource = host + imageSource;
            }
    
            if (_imagesWithSize.firstWhereOrNull(
                  (element) => element.imageUrl == imageSource,
                ) ==
                null) {
              Size size = await _calculateImageDimension(imageSource);
              _imagesWithSize.add(
                ImageWithSize(
                  imageSource,
                  size,
                ),
              );
            }
          }
        }
        _imagesWithSize.sort(
          (a, b) => (b.imageSize.height * b.imageSize.width).compareTo(
            a.imageSize.height * a.imageSize.width,
          ),
        );
    
        print(_imagesWithSize.first.imageUrl);
        print(_imagesWithSize[1].imageUrl);
        print(_imagesWithSize[2].imageUrl);
        _stopwatch.stop();
        print('executed in ${_stopwatch.elapsed}');
      }
    }
    
    Future<Size> _calculateImageDimension(String imageUrl) {
      Completer<Size> completer = Completer();
      Image image = Image.network(imageUrl);
      image.image.resolve(ImageConfiguration()).addListener(
        ImageStreamListener(
          (ImageInfo image, bool synchronousCall) {
            var myImage = image.image;
            Size size = Size(myImage.width.toDouble(), myImage.height.toDouble());
            completer.complete(size);
          },
        ),
      );
    
      return completer.future;
    }
    
    class ImageWithSize {
      final String imageUrl;
      final Size imageSize;
    
      ImageWithSize(this.imageUrl, this.imageSize);
    }
    
  • Chris
    Chris about 2 years
    I didnt know my images where chaning or animating but your hacky solution is working! Tbh I don't really understand the _calculateImageDimension, I just copied it. So feel free to elaborate what exactly happens here :D Anyway: Thanks for your help!