Home > Mobile >  Flutter ImageStream Bad state: Future already completed
Flutter ImageStream Bad state: Future already completed

Time:04-06

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);
}

CodePudding user response:

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.

  • Related