A Flutter plugin that provides a WebView widget.
On iOS the WebView widget is backed by a WKWebView; On Android the WebView widget is backed by a WebView.
Android | iOS | |
---|---|---|
Support | SDK 19+ or 20+ | 9.0+ |
Here is how you use it:
Step 1: Install it
Declare it in your pubspec.yaml
as shown below, then flutter pub get
.
dependencies:
flutter:
sdk: flutter
webview_flutter:
Android Platform Views
This plugin uses Platform Views to embed the Android’s webview within the Flutter app. It supports two modes: hybrid composition (the current default) and virtual display.
Here are some points to consider when choosing between the two:
- Hybrid composition has built-in keyboard support while virtual display has multiple keyboard issues.
- Hybrid composition requires Android SDK 19+ while virtual display requires Android SDK 20+.
- Hybrid composition and virtual display have different performance tradeoffs.
Using Hybrid Composition
The mode is currently enabled by default. You should however make sure to set the correct minSdkVersion
in android/app/build.gradle
if it was previously lower than 19:
android {
defaultConfig {
minSdkVersion 19
}
}
Using Virtual displays
-
Set the correct
minSdkVersion
inandroid/app/build.gradle
(if it was previously lower than 20):android { defaultConfig { minSdkVersion 20 } }
-
Set
WebView.platform = AndroidWebView();
ininitState()
. For example:import 'dart:io'; import 'package:webview_flutter/webview_flutter.dart'; class WebViewExample extends StatefulWidget { @override WebViewExampleState createState() => WebViewExampleState(); } class WebViewExampleState extends State<WebViewExample> { @override void initState() { super.initState(); // Enable virtual display. if (Platform.isAndroid) WebView.platform = AndroidWebView(); } @override Widget build(BuildContext context) { return WebView( initialUrl: 'https://flutter.dev', ); } }
Enable Material Components for Android
To use Material Components when the user interacts with input elements in the WebView, follow the steps described in the Enabling Material Components instructions.
Setting custom headers on POST requests
Currently, setting custom headers when making a post request with the WebViewController's loadRequest
method is not supported on Android. If you require this functionality, a workaround is to make the request manually, and then load the response data using loadHTMLString
instead.
Full Example
Here is a full example:
- Declare
webview_flutter
in your pubspec.yaml as we had described earlier. - Replace your
main.dart
with the following code.
main.dart
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:path_provider/path_provider.dart';
import 'package:webview_flutter/webview_flutter.dart';
void main() => runApp(const MaterialApp(home: WebViewExample()));
const String kNavigationExamplePage = '''
<!DOCTYPE html><html>
<head><title>Navigation Delegate Example</title></head>
<body>
<p>
The navigation delegate is set to block navigation to the youtube website.
</p>
<ul>
<ul><a href="https://www.youtube.com/">https://www.youtube.com/</a></ul>
<ul><a href="https://www.google.com/">https://www.google.com/</a></ul>
</ul>
</body>
</html>
''';
const String kLocalExamplePage = '''
<!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>
''';
const String kTransparentBackgroundPage = '''
<!DOCTYPE html>
<html>
<head>
<title>Transparent background test</title>
</head>
<style type="text/css">
body { background: transparent; margin: 0; padding: 0; }
#container { position: relative; margin: 0; padding: 0; width: 100vw; height: 100vh; }
#shape { background: red; width: 200px; height: 200px; margin: 0; padding: 0; position: absolute; top: calc(50% - 100px); left: calc(50% - 100px); }
p { text-align: center; }
</style>
<body>
<div id="container">
<p>Transparent background test</p>
<div id="shape"></div>
</div>
</body>
</html>
''';
class WebViewExample extends StatefulWidget {
const WebViewExample({Key? key, this.cookieManager}) : super(key: key);
final CookieManager? cookieManager;
@override
State<WebViewExample> createState() => _WebViewExampleState();
}
class _WebViewExampleState extends State<WebViewExample> {
final Completer<WebViewController> _controller =
Completer<WebViewController>();
@override
void initState() {
super.initState();
if (Platform.isAndroid) {
WebView.platform = SurfaceAndroidWebView();
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.green,
appBar: AppBar(
title: const Text('Flutter WebView example'),
// This drop down menu demonstrates that Flutter widgets can be shown over the web view.
actions: <Widget>[
NavigationControls(_controller.future),
SampleMenu(_controller.future, widget.cookieManager),
],
),
body: WebView(
initialUrl: 'https://flutter.dev',
javascriptMode: JavascriptMode.unrestricted,
onWebViewCreated: (WebViewController webViewController) {
_controller.complete(webViewController);
},
onProgress: (int progress) {
print('WebView is loading (progress : $progress%)');
},
javascriptChannels: <JavascriptChannel>{
_toasterJavascriptChannel(context),
},
navigationDelegate: (NavigationRequest request) {
if (request.url.startsWith('https://www.youtube.com/')) {
print('blocking navigation to $request}');
return NavigationDecision.prevent;
}
print('allowing navigation to $request');
return NavigationDecision.navigate;
},
onPageStarted: (String url) {
print('Page started loading: $url');
},
onPageFinished: (String url) {
print('Page finished loading: $url');
},
gestureNavigationEnabled: true,
backgroundColor: const Color(0x00000000),
),
floatingActionButton: favoriteButton(),
);
}
JavascriptChannel _toasterJavascriptChannel(BuildContext context) {
return JavascriptChannel(
name: 'Toaster',
onMessageReceived: (JavascriptMessage message) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(message.message)),
);
});
}
Widget favoriteButton() {
return FutureBuilder<WebViewController>(
future: _controller.future,
builder: (BuildContext context,
AsyncSnapshot<WebViewController> controller) {
return FloatingActionButton(
onPressed: () async {
String? url;
if (controller.hasData) {
url = await controller.data!.currentUrl();
}
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
controller.hasData
? 'Favorited $url'
: 'Unable to favorite',
),
),
);
},
child: const Icon(Icons.favorite),
);
});
}
}
enum MenuOptions {
showUserAgent,
listCookies,
clearCookies,
addToCache,
listCache,
clearCache,
navigationDelegate,
doPostRequest,
loadLocalFile,
loadFlutterAsset,
loadHtmlString,
transparentBackground,
setCookie,
}
class SampleMenu extends StatelessWidget {
SampleMenu(this.controller, CookieManager? cookieManager, {Key? key})
: cookieManager = cookieManager ?? CookieManager(),
super(key: key);
final Future<WebViewController> controller;
late final CookieManager cookieManager;
@override
Widget build(BuildContext context) {
return FutureBuilder<WebViewController>(
future: controller,
builder:
(BuildContext context, AsyncSnapshot<WebViewController> controller) {
return PopupMenuButton<MenuOptions>(
key: const ValueKey<String>('ShowPopupMenu'),
onSelected: (MenuOptions value) {
switch (value) {
case MenuOptions.showUserAgent:
_onShowUserAgent(controller.data!, context);
break;
case MenuOptions.listCookies:
_onListCookies(controller.data!, context);
break;
case MenuOptions.clearCookies:
_onClearCookies(context);
break;
case MenuOptions.addToCache:
_onAddToCache(controller.data!, context);
break;
case MenuOptions.listCache:
_onListCache(controller.data!, context);
break;
case MenuOptions.clearCache:
_onClearCache(controller.data!, context);
break;
case MenuOptions.navigationDelegate:
_onNavigationDelegateExample(controller.data!, context);
break;
case MenuOptions.doPostRequest:
_onDoPostRequest(controller.data!, context);
break;
case MenuOptions.loadLocalFile:
_onLoadLocalFileExample(controller.data!, context);
break;
case MenuOptions.loadFlutterAsset:
_onLoadFlutterAssetExample(controller.data!, context);
break;
case MenuOptions.loadHtmlString:
_onLoadHtmlStringExample(controller.data!, context);
break;
case MenuOptions.transparentBackground:
_onTransparentBackground(controller.data!, context);
break;
case MenuOptions.setCookie:
_onSetCookie(controller.data!, context);
break;
}
},
itemBuilder: (BuildContext context) => <PopupMenuItem<MenuOptions>>[
PopupMenuItem<MenuOptions>(
value: MenuOptions.showUserAgent,
enabled: controller.hasData,
child: const Text('Show user agent'),
),
const PopupMenuItem<MenuOptions>(
value: MenuOptions.listCookies,
child: Text('List cookies'),
),
const PopupMenuItem<MenuOptions>(
value: MenuOptions.clearCookies,
child: Text('Clear cookies'),
),
const PopupMenuItem<MenuOptions>(
value: MenuOptions.addToCache,
child: Text('Add to cache'),
),
const PopupMenuItem<MenuOptions>(
value: MenuOptions.listCache,
child: Text('List cache'),
),
const PopupMenuItem<MenuOptions>(
value: MenuOptions.clearCache,
child: Text('Clear cache'),
),
const PopupMenuItem<MenuOptions>(
value: MenuOptions.navigationDelegate,
child: Text('Navigation Delegate example'),
),
const PopupMenuItem<MenuOptions>(
value: MenuOptions.doPostRequest,
child: Text('Post Request'),
),
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.loadFlutterAsset,
child: Text('Load Flutter Asset'),
),
const PopupMenuItem<MenuOptions>(
key: ValueKey<String>('ShowTransparentBackgroundExample'),
value: MenuOptions.transparentBackground,
child: Text('Transparent background example'),
),
const PopupMenuItem<MenuOptions>(
value: MenuOptions.setCookie,
child: Text('Set cookie'),
),
],
);
},
);
}
Future<void> _onShowUserAgent(
WebViewController controller, BuildContext context) async {
// Send a message with the user agent string to the Toaster JavaScript channel we registered
// with the WebView.
await controller.runJavascript(
'Toaster.postMessage("User Agent: " + navigator.userAgent);');
}
Future<void> _onListCookies(
WebViewController controller, BuildContext context) async {
final String cookies =
await controller.runJavascriptReturningResult('document.cookie');
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
content: Column(
mainAxisAlignment: MainAxisAlignment.end,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
const Text('Cookies:'),
_getCookieList(cookies),
],
),
));
}
Future<void> _onAddToCache(
WebViewController controller, BuildContext context) async {
await controller.runJavascript(
'caches.open("test_caches_entry"); localStorage["test_localStorage"] = "dummy_entry";');
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(
content: Text('Added a test entry to cache.'),
));
}
Future<void> _onListCache(
WebViewController controller, BuildContext context) async {
await controller.runJavascript('caches.keys()'
// ignore: missing_whitespace_between_adjacent_strings
'.then((cacheKeys) => JSON.stringify({"cacheKeys" : cacheKeys, "localStorage" : localStorage}))'
'.then((caches) => Toaster.postMessage(caches))');
}
Future<void> _onClearCache(
WebViewController controller, BuildContext context) async {
await controller.clearCache();
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(
content: Text('Cache cleared.'),
));
}
Future<void> _onClearCookies(BuildContext context) async {
final bool hadCookies = await cookieManager.clearCookies();
String message = 'There were cookies. Now, they are gone!';
if (!hadCookies) {
message = 'There are no cookies.';
}
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
content: Text(message),
));
}
Future<void> _onNavigationDelegateExample(
WebViewController controller, BuildContext context) async {
final String contentBase64 =
base64Encode(const Utf8Encoder().convert(kNavigationExamplePage));
await controller.loadUrl('data:text/html;base64,$contentBase64');
}
Future<void> _onSetCookie(
WebViewController controller, BuildContext context) async {
await cookieManager.setCookie(
const WebViewCookie(
name: 'foo', value: 'bar', domain: 'httpbin.org', path: '/anything'),
);
await controller.loadUrl('https://httpbin.org/anything');
}
Future<void> _onDoPostRequest(
WebViewController controller, BuildContext context) async {
final WebViewRequest request = WebViewRequest(
uri: Uri.parse('https://httpbin.org/post'),
method: WebViewRequestMethod.post,
headers: <String, String>{'foo': 'bar', 'Content-Type': 'text/plain'},
body: Uint8List.fromList('Test Body'.codeUnits),
);
await controller.loadRequest(request);
}
Future<void> _onLoadLocalFileExample(
WebViewController controller, BuildContext context) async {
final String pathToIndex = await _prepareLocalFile();
await controller.loadFile(pathToIndex);
}
Future<void> _onLoadFlutterAssetExample(
WebViewController controller, BuildContext context) async {
await controller.loadFlutterAsset('assets/www/index.html');
}
Future<void> _onLoadHtmlStringExample(
WebViewController controller, BuildContext context) async {
await controller.loadHtmlString(kLocalExamplePage);
}
Future<void> _onTransparentBackground(
WebViewController controller, BuildContext context) async {
await controller.loadHtmlString(kTransparentBackgroundPage);
}
Widget _getCookieList(String cookies) {
if (cookies == null || cookies == '""') {
return Container();
}
final List<String> cookieList = cookies.split(';');
final Iterable<Text> cookieWidgets =
cookieList.map((String cookie) => Text(cookie));
return Column(
mainAxisAlignment: MainAxisAlignment.end,
mainAxisSize: MainAxisSize.min,
children: cookieWidgets.toList(),
);
}
static Future<String> _prepareLocalFile() async {
final String tmpDir = (await getTemporaryDirectory()).path;
final File indexFile = File(
<String>{tmpDir, 'www', 'index.html'}.join(Platform.pathSeparator));
await indexFile.create(recursive: true);
await indexFile.writeAsString(kLocalExamplePage);
return indexFile.path;
}
}
class NavigationControls extends StatelessWidget {
const NavigationControls(this._webViewControllerFuture, {Key? key})
: assert(_webViewControllerFuture != null),
super(key: key);
final Future<WebViewController> _webViewControllerFuture;
@override
Widget build(BuildContext context) {
return FutureBuilder<WebViewController>(
future: _webViewControllerFuture,
builder:
(BuildContext context, AsyncSnapshot<WebViewController> snapshot) {
final bool webViewReady =
snapshot.connectionState == ConnectionState.done;
final WebViewController? controller = snapshot.data;
return Row(
children: <Widget>[
IconButton(
icon: const Icon(Icons.arrow_back_ios),
onPressed: !webViewReady
? null
: () async {
if (await controller!.canGoBack()) {
await controller.goBack();
} else {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('No back history item')),
);
return;
}
},
),
IconButton(
icon: const Icon(Icons.arrow_forward_ios),
onPressed: !webViewReady
? null
: () async {
if (await controller!.canGoForward()) {
await controller.goForward();
} else {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('No forward history item')),
);
return;
}
},
),
IconButton(
icon: const Icon(Icons.replay),
onPressed: !webViewReady
? null
: () {
controller!.reload();
},
),
],
);
},
);
}
}