[webview_flutter] Implementations of `loadFile` and `loadHtmlString` for WKWebView (#4486)
diff --git a/packages/webview_flutter/webview_flutter_wkwebview/CHANGELOG.md b/packages/webview_flutter/webview_flutter_wkwebview/CHANGELOG.md
index c1b19fd..cbf1f04 100644
--- a/packages/webview_flutter/webview_flutter_wkwebview/CHANGELOG.md
+++ b/packages/webview_flutter/webview_flutter_wkwebview/CHANGELOG.md
@@ -1,3 +1,7 @@
+## 2.4.0
+
+* Implemented new `loadFile` and `loadHtmlString` methods from the platform interface.
+
## 2.3.0
* Implemented new `loadRequest` method from platform interface.
diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner.xcodeproj/project.pbxproj b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner.xcodeproj/project.pbxproj
index 2be87fb..e292b1b 100644
--- a/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner.xcodeproj/project.pbxproj
+++ b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner.xcodeproj/project.pbxproj
@@ -3,7 +3,7 @@
archiveVersion = 1;
classes = {
};
- objectVersion = 46;
+ objectVersion = 50;
objects = {
/* Begin PBXBuildFile section */
@@ -273,7 +273,7 @@
isa = PBXProject;
attributes = {
DefaultBuildSystemTypeForWorkspace = Original;
- LastUpgradeCheck = 1030;
+ LastUpgradeCheck = 1300;
ORGANIZATIONNAME = "The Flutter Authors";
TargetAttributes = {
68BDCAE823C3F7CB00D9C032 = {
diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
index d7453a8..cb713d7 100644
--- a/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
+++ b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
- LastUpgradeVersion = "1030"
+ LastUpgradeVersion = "1300"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/ios/RunnerTests/FLTWebViewTests.m b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/RunnerTests/FLTWebViewTests.m
index 61e43c1..a3c314a 100644
--- a/packages/webview_flutter/webview_flutter_wkwebview/example/ios/RunnerTests/FLTWebViewTests.m
+++ b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/RunnerTests/FLTWebViewTests.m
@@ -89,6 +89,201 @@
}
}
+- (void)testLoadFileSucceeds {
+ NSString *testFilePath = @"/assets/file.html";
+ NSURL *url = [NSURL fileURLWithPath:testFilePath isDirectory:NO];
+ XCTestExpectation *resultExpectation =
+ [self expectationWithDescription:@"Should return successful result over the method channel."];
+ FLTWebViewController *controller =
+ [[FLTWebViewController alloc] initWithFrame:CGRectMake(0, 0, 300, 400)
+ viewIdentifier:1
+ arguments:nil
+ binaryMessenger:self.mockBinaryMessenger];
+ FLTWKWebView *mockWebView = OCMClassMock(FLTWKWebView.class);
+ controller.webView = mockWebView;
+ [controller onMethodCall:[FlutterMethodCall methodCallWithMethodName:@"loadFile"
+ arguments:testFilePath]
+ result:^(id _Nullable result) {
+ XCTAssertNil(result);
+ [resultExpectation fulfill];
+ }];
+
+ [self waitForExpectations:@[ resultExpectation ] timeout:30.0];
+ OCMVerify([mockWebView loadFileURL:url
+ allowingReadAccessToURL:[url URLByDeletingLastPathComponent]]);
+}
+
+- (void)testLoadFileFailsWithInvalidPath {
+ NSArray *resultExpectations = @[
+ [self expectationWithDescription:@"Should return failed result when argument is nil."],
+ [self expectationWithDescription:
+ @"Should return failed result when argument is not of type NSString*."],
+ [self expectationWithDescription:
+ @"Should return failed result when argument is an empty string."],
+ ];
+
+ FLTWebViewController *controller =
+ [[FLTWebViewController alloc] initWithFrame:CGRectMake(0, 0, 300, 400)
+ viewIdentifier:1
+ arguments:nil
+ binaryMessenger:self.mockBinaryMessenger];
+ FLTWKWebView *mockWebView = OCMClassMock(FLTWKWebView.class);
+ controller.webView = mockWebView;
+ [controller onMethodCall:[FlutterMethodCall methodCallWithMethodName:@"loadFile" arguments:nil]
+ result:^(id _Nullable result) {
+ FlutterError *expected =
+ [FlutterError errorWithCode:@"loadFile_failed"
+ message:@"Failed parsing file path."
+ details:@"Argument is nil."];
+ [FLTWebViewTests assertFlutterError:result withExpected:expected];
+ [resultExpectations[0] fulfill];
+ }];
+ [controller onMethodCall:[FlutterMethodCall methodCallWithMethodName:@"loadFile" arguments:@(10)]
+ result:^(id _Nullable result) {
+ FlutterError *expected =
+ [FlutterError errorWithCode:@"loadFile_failed"
+ message:@"Failed parsing file path."
+ details:@"Argument is not of type NSString."];
+ [FLTWebViewTests assertFlutterError:result withExpected:expected];
+ [resultExpectations[1] fulfill];
+ }];
+ [controller onMethodCall:[FlutterMethodCall methodCallWithMethodName:@"loadFile" arguments:@""]
+ result:^(id _Nullable result) {
+ FlutterError *expected =
+ [FlutterError errorWithCode:@"loadFile_failed"
+ message:@"Failed parsing file path."
+ details:@"Argument contains an empty string."];
+ [FLTWebViewTests assertFlutterError:result withExpected:expected];
+ [resultExpectations[2] fulfill];
+ }];
+
+ [self waitForExpectations:resultExpectations timeout:1.0];
+ OCMReject([mockWebView loadFileURL:[OCMArg any] allowingReadAccessToURL:[OCMArg any]]);
+}
+
+- (void)testLoadFileSucceedsWithBaseUrl {
+ NSURL *baseUrl = [NSURL URLWithString:@"https://flutter.dev"];
+ XCTestExpectation *resultExpectation =
+ [self expectationWithDescription:@"Should return successful result over the method channel."];
+ FLTWebViewController *controller =
+ [[FLTWebViewController alloc] initWithFrame:CGRectMake(0, 0, 300, 400)
+ viewIdentifier:1
+ arguments:nil
+ binaryMessenger:self.mockBinaryMessenger];
+ FLTWKWebView *mockWebView = OCMClassMock(FLTWKWebView.class);
+ controller.webView = mockWebView;
+ [controller onMethodCall:[FlutterMethodCall methodCallWithMethodName:@"loadHtmlString"
+ arguments:@{
+ @"html" : @"some HTML string",
+ @"baseUrl" : @"https://flutter.dev"
+ }]
+ result:^(id _Nullable result) {
+ XCTAssertNil(result);
+ [resultExpectation fulfill];
+ }];
+
+ [self waitForExpectations:@[ resultExpectation ] timeout:30.0];
+ OCMVerify([mockWebView loadHTMLString:@"some HTML string" baseURL:baseUrl]);
+}
+
+- (void)testLoadFileSucceedsWithoutBaseUrl {
+ XCTestExpectation *resultExpectation =
+ [self expectationWithDescription:@"Should return successful result over the method channel."];
+ FLTWebViewController *controller =
+ [[FLTWebViewController alloc] initWithFrame:CGRectMake(0, 0, 300, 400)
+ viewIdentifier:1
+ arguments:nil
+ binaryMessenger:self.mockBinaryMessenger];
+ FLTWKWebView *mockWebView = OCMClassMock(FLTWKWebView.class);
+ controller.webView = mockWebView;
+ [controller
+ onMethodCall:[FlutterMethodCall methodCallWithMethodName:@"loadHtmlString"
+ arguments:@{@"html" : @"some HTML string"}]
+ result:^(id _Nullable result) {
+ XCTAssertNil(result);
+ [resultExpectation fulfill];
+ }];
+
+ [self waitForExpectations:@[ resultExpectation ] timeout:30.0];
+ OCMVerify([mockWebView loadHTMLString:@"some HTML string" baseURL:nil]);
+}
+
+- (void)testLoadHtmlStringFailsWithInvalidArgument {
+ NSArray *resultExpectations = @[
+ [self expectationWithDescription:@"Should return failed result when argument is nil."],
+ [self expectationWithDescription:
+ @"Should return failed result when argument is not of type NSDictionary*."],
+ [self expectationWithDescription:@"Should return failed result when HTML argument is nil."],
+ [self expectationWithDescription:
+ @"Should return failed result when HTML argument is not of type NSString*."],
+ [self expectationWithDescription:
+ @"Should return failed result when HTML argument is an empty string."],
+ ];
+
+ FLTWebViewController *controller =
+ [[FLTWebViewController alloc] initWithFrame:CGRectMake(0, 0, 300, 400)
+ viewIdentifier:1
+ arguments:nil
+ binaryMessenger:self.mockBinaryMessenger];
+ FLTWKWebView *mockWebView = OCMClassMock(FLTWKWebView.class);
+ controller.webView = mockWebView;
+ FlutterError *expected = [FlutterError
+ errorWithCode:@"loadHtmlString_failed"
+ message:@"Failed parsing arguments."
+ details:@"Arguments should be a dictionary containing at least a 'html' element and "
+ @"optionally a 'baseUrl' argument. For example: `@{ @\"html\": @\"some html "
+ @"code\", @\"baseUrl\": @\"https://flutter.dev\" }`"];
+ [controller onMethodCall:[FlutterMethodCall methodCallWithMethodName:@"loadHtmlString"
+ arguments:nil]
+ result:^(id _Nullable result) {
+ [FLTWebViewTests assertFlutterError:result withExpected:expected];
+ [resultExpectations[0] fulfill];
+ }];
+ [controller onMethodCall:[FlutterMethodCall methodCallWithMethodName:@"loadHtmlString"
+ arguments:@""]
+ result:^(id _Nullable result) {
+ [FLTWebViewTests assertFlutterError:result withExpected:expected];
+ [resultExpectations[1] fulfill];
+ }];
+ [controller onMethodCall:[FlutterMethodCall methodCallWithMethodName:@"loadHtmlString"
+ arguments:@{}]
+ result:^(id _Nullable result) {
+ FlutterError *expected =
+ [FlutterError errorWithCode:@"loadHtmlString_failed"
+ message:@"Failed parsing HTML string argument."
+ details:@"Argument is nil."];
+ [FLTWebViewTests assertFlutterError:result withExpected:expected];
+ [resultExpectations[2] fulfill];
+ }];
+ [controller onMethodCall:[FlutterMethodCall methodCallWithMethodName:@"loadHtmlString"
+ arguments:@{
+ @"html" : @(42),
+ }]
+ result:^(id _Nullable result) {
+ FlutterError *expected =
+ [FlutterError errorWithCode:@"loadHtmlString_failed"
+ message:@"Failed parsing HTML string argument."
+ details:@"Argument is not of type NSString."];
+ [FLTWebViewTests assertFlutterError:result withExpected:expected];
+ [resultExpectations[3] fulfill];
+ }];
+ [controller onMethodCall:[FlutterMethodCall methodCallWithMethodName:@"loadHtmlString"
+ arguments:@{
+ @"html" : @"",
+ }]
+ result:^(id _Nullable result) {
+ FlutterError *expected =
+ [FlutterError errorWithCode:@"loadHtmlString_failed"
+ message:@"Failed parsing HTML string argument."
+ details:@"Argument contains an empty string."];
+ [FLTWebViewTests assertFlutterError:result withExpected:expected];
+ [resultExpectations[4] fulfill];
+ }];
+
+ [self waitForExpectations:resultExpectations timeout:1.0];
+ OCMReject([mockWebView loadHTMLString:[OCMArg any] baseURL:[OCMArg any]]);
+}
+
- (void)testRunJavascriptFailsForNullString {
// Setup
FLTWebViewController *controller =
@@ -302,6 +497,14 @@
[self waitForExpectationsWithTimeout:30.0 handler:nil];
}
++ (void)assertFlutterError:(id)actual withExpected:(FlutterError *)expected {
+ XCTAssertTrue([actual class] == [FlutterError class]);
+ FlutterError *errorResult = actual;
+ XCTAssertEqualObjects(errorResult.code, expected.code);
+ XCTAssertEqualObjects(errorResult.message, expected.message);
+ XCTAssertEqualObjects(errorResult.details, expected.details);
+}
+
- (void)testBuildNSURLRequestReturnsNilForNonDictionaryValue {
// Setup
FLTWebViewController *controller =
diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/lib/main.dart b/packages/webview_flutter/webview_flutter_wkwebview/example/lib/main.dart
index ea95cdd..72168ec 100644
--- a/packages/webview_flutter/webview_flutter_wkwebview/example/lib/main.dart
+++ b/packages/webview_flutter/webview_flutter_wkwebview/example/lib/main.dart
@@ -6,9 +6,12 @@
import 'dart:async';
import 'dart:convert';
+import 'dart:io';
import 'dart:typed_data';
+
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
+import 'package:path_provider/path_provider.dart';
import 'package:webview_flutter_platform_interface/webview_flutter_platform_interface.dart';
import 'navigation_decision.dart';
@@ -34,6 +37,25 @@
</html>
''';
+const String kLocalFileExamplePage = '''
+<!DOCTYPE html>
+<html lang="en">
+<head>
+<title>Load file or HTML string example</title>
+</head>
+<body>
+
+<h1>Local demo page</h1>
+<p>
+ This is an example page used to demonstrate how to load a local file or HTML
+ string using the <a href="https://pub.dev/packages/webview_flutter">Flutter
+ webview</a> plugin.
+</p>
+
+</body>
+</html>
+''';
+
class _WebViewExample extends StatefulWidget {
const _WebViewExample({Key? key}) : super(key: key);
@@ -121,6 +143,8 @@
listCache,
clearCache,
navigationDelegate,
+ loadLocalFile,
+ loadHtmlString,
doPostRequest,
}
@@ -159,6 +183,12 @@
case _MenuOptions.navigationDelegate:
_onNavigationDelegateExample(controller.data!, context);
break;
+ case _MenuOptions.loadLocalFile:
+ _onLoadLocalFileExample(controller.data!, context);
+ break;
+ case _MenuOptions.loadHtmlString:
+ _onLoadHtmlStringExample(controller.data!, context);
+ break;
case _MenuOptions.doPostRequest:
_onDoPostRequest(controller.data!, context);
break;
@@ -195,6 +225,14 @@
child: Text('Navigation Delegate example'),
),
const PopupMenuItem<_MenuOptions>(
+ value: _MenuOptions.loadHtmlString,
+ child: Text('Load HTML string'),
+ ),
+ const PopupMenuItem<_MenuOptions>(
+ value: _MenuOptions.loadLocalFile,
+ child: Text('Load local file'),
+ ),
+ const PopupMenuItem<_MenuOptions>(
value: _MenuOptions.doPostRequest,
child: Text('Post Request'),
),
@@ -268,6 +306,18 @@
await controller.loadUrl('data:text/html;base64,$contentBase64');
}
+ void _onLoadLocalFileExample(
+ WebViewController controller, BuildContext context) async {
+ String pathToIndex = await _prepareLocalFile();
+
+ await controller.loadFile(pathToIndex);
+ }
+
+ void _onLoadHtmlStringExample(
+ WebViewController controller, BuildContext context) async {
+ await controller.loadHtmlString(kLocalFileExamplePage);
+ }
+
void _onDoPostRequest(
WebViewController controller, BuildContext context) async {
WebViewRequest request = WebViewRequest(
@@ -292,6 +342,16 @@
children: cookieWidgets.toList(),
);
}
+
+ static Future<String> _prepareLocalFile() async {
+ final String tmpDir = (await getTemporaryDirectory()).path;
+ File indexFile = File('$tmpDir/www/index.html');
+
+ await Directory('$tmpDir/www').create(recursive: true);
+ await indexFile.writeAsString(kLocalFileExamplePage);
+
+ return indexFile.path;
+ }
}
class _NavigationControls extends StatelessWidget {
diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/lib/web_view.dart b/packages/webview_flutter/webview_flutter_wkwebview/example/lib/web_view.dart
index b2555cd..ab4b773 100644
--- a/packages/webview_flutter/webview_flutter_wkwebview/example/lib/web_view.dart
+++ b/packages/webview_flutter/webview_flutter_wkwebview/example/lib/web_view.dart
@@ -305,6 +305,35 @@
WebView _widget;
+ /// Loads the file located on the specified [absoluteFilePath].
+ ///
+ /// The [absoluteFilePath] parameter should contain the absolute path to the
+ /// file as it is stored on the device. For example:
+ /// `/Users/username/Documents/www/index.html`.
+ ///
+ /// Throws an ArgumentError if the [absoluteFilePath] does not exist.
+ Future<void> loadFile(
+ String absoluteFilePath,
+ ) {
+ assert(absoluteFilePath.isNotEmpty);
+ return _webViewPlatformController.loadFile(absoluteFilePath);
+ }
+
+ /// Loads the supplied HTML string.
+ ///
+ /// The [baseUrl] parameter is used when resolving relative URLs within the
+ /// HTML string.
+ Future<void> loadHtmlString(
+ String html, {
+ String? baseUrl,
+ }) {
+ assert(html.isNotEmpty);
+ return _webViewPlatformController.loadHtmlString(
+ html,
+ baseUrl: baseUrl,
+ );
+ }
+
/// Loads the specified URL.
///
/// If `headers` is not null and the URL is an HTTP URL, the key value paris in `headers` will
diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/pubspec.yaml b/packages/webview_flutter/webview_flutter_wkwebview/example/pubspec.yaml
index 229da5e..c8001c8 100644
--- a/packages/webview_flutter/webview_flutter_wkwebview/example/pubspec.yaml
+++ b/packages/webview_flutter/webview_flutter_wkwebview/example/pubspec.yaml
@@ -8,6 +8,9 @@
dependencies:
flutter:
sdk: flutter
+
+ path_provider: ^2.0.6
+
webview_flutter_wkwebview:
# When depending on this package from a real application you should use:
# webview_flutter: ^x.y.z
@@ -31,3 +34,4 @@
assets:
- assets/sample_audio.ogg
- assets/sample_video.mp4
+
\ No newline at end of file
diff --git a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FlutterWebView.m b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FlutterWebView.m
index b8355ad..351d1ae 100644
--- a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FlutterWebView.m
+++ b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FlutterWebView.m
@@ -140,6 +140,10 @@
- (void)onMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result {
if ([[call method] isEqualToString:@"updateSettings"]) {
[self onUpdateSettings:call result:result];
+ } else if ([[call method] isEqualToString:@"loadFile"]) {
+ [self onLoadFile:call result:result];
+ } else if ([[call method] isEqualToString:@"loadHtmlString"]) {
+ [self onLoadHtmlString:call result:result];
} else if ([[call method] isEqualToString:@"loadUrl"]) {
[self onLoadUrl:call result:result];
} else if ([[call method] isEqualToString:@"loadRequest"]) {
@@ -192,6 +196,60 @@
result([FlutterError errorWithCode:@"updateSettings_failed" message:error details:nil]);
}
+- (void)onLoadFile:(FlutterMethodCall*)call result:(FlutterResult)result {
+ NSString* error = nil;
+ if (![FLTWebViewController isValidStringArgument:[call arguments] withErrorMessage:&error]) {
+ result([FlutterError errorWithCode:@"loadFile_failed"
+ message:@"Failed parsing file path."
+ details:error]);
+ return;
+ }
+
+ NSURL* url = [NSURL fileURLWithPath:[call arguments] isDirectory:NO];
+
+ if (!url) {
+ NSString* errorDetails = [NSString stringWithFormat:@"Initializing NSURL with the supplied "
+ @"'%@' path resulted in a nil value.",
+ [call arguments]];
+ result([FlutterError errorWithCode:@"loadFile_failed"
+ message:@"Failed parsing file path."
+ details:errorDetails]);
+ return;
+ }
+
+ NSURL* baseUrl = [url URLByDeletingLastPathComponent];
+
+ [_webView loadFileURL:url allowingReadAccessToURL:baseUrl];
+ result(nil);
+}
+
+- (void)onLoadHtmlString:(FlutterMethodCall*)call result:(FlutterResult)result {
+ NSDictionary* arguments = [call arguments];
+ if (![arguments isKindOfClass:NSDictionary.class]) {
+ result([FlutterError
+ errorWithCode:@"loadHtmlString_failed"
+ message:@"Failed parsing arguments."
+ details:@"Arguments should be a dictionary containing at least a 'html' element and "
+ @"optionally a 'baseUrl' argument. For example: `@{ @\"html\": @\"some html "
+ @"code\", @\"baseUrl\": @\"https://flutter.dev\" }`"]);
+ return;
+ }
+
+ NSString* htmlString = [call arguments][@"html"];
+ NSString* baseUrl =
+ [call arguments][@"baseUrl"] == [NSNull null] ? nil : [call arguments][@"baseUrl"];
+ NSString* error = nil;
+ if (![FLTWebViewController isValidStringArgument:htmlString withErrorMessage:&error]) {
+ result([FlutterError errorWithCode:@"loadHtmlString_failed"
+ message:@"Failed parsing HTML string argument."
+ details:error]);
+ return;
+ }
+
+ [_webView loadHTMLString:htmlString baseURL:[NSURL URLWithString:baseUrl]];
+ result(nil);
+}
+
- (void)onLoadUrl:(FlutterMethodCall*)call result:(FlutterResult)result {
NSMutableDictionary* requestData = [[NSMutableDictionary alloc] init];
if (call.arguments[@"url"]) {
@@ -556,6 +614,37 @@
}
}
+/**
+ * Validates if the given `argument` is a non-null, non-empty string.
+ *
+ * @param argument The argument that should be validated.
+ * @param errorDetails An optional NSString variable which will contain a detailed error message in
+ * case the supplied argument is not valid.
+ * @return `YES` if the given `argument` is a valid non-null, non-empty string; otherwise `NO`.
+ */
++ (BOOL)isValidStringArgument:(id)argument withErrorMessage:(NSString**)errorDetails {
+ if (!argument) {
+ if (errorDetails) {
+ *errorDetails = @"Argument is nil.";
+ }
+ return NO;
+ }
+ if (![argument isKindOfClass:NSString.class]) {
+ if (errorDetails) {
+ *errorDetails = @"Argument is not of type NSString.";
+ }
+ return NO;
+ }
+ if (![argument length]) {
+ if (errorDetails) {
+ *errorDetails = @"Argument contains an empty string.";
+ }
+ return NO;
+ }
+
+ return YES;
+}
+
#pragma mark WKUIDelegate
- (WKWebView*)webView:(WKWebView*)webView
diff --git a/packages/webview_flutter/webview_flutter_wkwebview/pubspec.yaml b/packages/webview_flutter/webview_flutter_wkwebview/pubspec.yaml
index ff2b69f..466c1a2 100644
--- a/packages/webview_flutter/webview_flutter_wkwebview/pubspec.yaml
+++ b/packages/webview_flutter/webview_flutter_wkwebview/pubspec.yaml
@@ -2,7 +2,7 @@
description: A Flutter plugin that provides a WebView widget based on Apple's WKWebView control.
repository: https://github.com/flutter/plugins/tree/master/packages/webview_flutter/webview_flutter_wkwebview
issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+webview%22
-version: 2.3.0
+version: 2.4.0
environment:
sdk: ">=2.14.0 <3.0.0"