I have an WebView
inside my app where I crawl the current Website.
This is the procedure:
- User taps on button
- crawl the content of the current URL
- get all the images
- for each image get its dimension
- 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
.