Flutter ImageStream 错误状态:未来已经完成

Flutter ImageStream Bad state: Future already completed

我的应用程序中有一个 WebView,我可以在其中抓取当前网站。

这是程序:

  1. 用户点击按钮
  2. 抓取当前URL
  3. 的内容
  4. 获取所有图片
  5. 获取每个图像的尺寸
  6. 打印出排序列表的前三个元素

问题是4:

这是我的代码:

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

这失败了:

Bad state: Future already completed

现在奇怪的是它只在某些 URL 上失败。

我的 _calculateImageDimension 怎么了?我错过了什么?

这是完整的代码:

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

如果 ImageStream 发出多次,您将调用 completer.complete() 两次,这是一个错误。根据 ImageStream 文档,如果图像是动画的,或者如果资源被更改,就会发生这种情况。

如果您只关心第一个发射,您可以等待 image.image.resolve(ImageConfiguration()).first。一个更 hacky 的解决方案是仅在 completer.isCompleted == false.

时调用 complete()