Skip to main content
Version: 6.x.x

InAppWebView

Supported Platforms: AndroidiOSmacOSWindowsWeb

InAppWebView is a Flutter Widget for adding an inline native WebView integrated into the flutter widget tree.

The plugin relies on Flutter's mechanism for embedding native views:

Known issues are tagged with the platform-views label in the Flutter official repo.

On Web platform, the WebView is embedded using the iframe HTML element.

Web platform limitation

Because of the nature of the iframe, the Web platform has a lot of limitations regarding WebView control. Most of the InAppWebViewController methods are impossible to be implemented, and the ones that are, require the iframe to have the same origin of the website.

Check the documentation of each specific method you want to use to understand if it is supported or not.

Basic Usage

InAppWebViewSettings provides access to a variety of settings that you might find useful. For example, if you're developing a web application that's designed specifically for the WebView in your app, then you can define a custom user agent string with InAppWebViewSettings.userAgent, then query the custom user agent in your web page to verify that the client requesting your web page is actually your app.

Instead, use InAppWebViewController to control the WebView instance.

Windows Platform

Before initializing the WebView, call the WebViewEnvironment.getAvailableVersion() static method to check whether the required WebView2 Runtime is installed or not on the user system.

WebView2 Runtime is ship in box with Windows 11, but it may not be installed on Windows 10 devices. If it isn't installed, the method will return null, so you need consider how to distribute the WebView2 Runtime to your users.

You can guide your user to install WebView2 Runtime from this page or choose one of the distribution method described in detail here: https://docs.microsoft.com/en-us/microsoft-edge/webview2/concepts/distribution

Also, on Windows Platform, you should create a WebViewEnvironment with a custom user data folder. The default user data folder of WebView2 is your_exe_file\WebView2, which is not a good place to store user data. For example, if the application is installed in a read-only directory, the application will crash when WebView2 tries to write data. After that, use it as the InAppWebView.webViewEnvironment argument when you create an InAppWebView widget.

Example:

import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_inappwebview/flutter_inappwebview.dart';
import 'package:url_launcher/url_launcher.dart';

WebViewEnvironment? webViewEnvironment;

Future main() async {
WidgetsFlutterBinding.ensureInitialized();

if (!kIsWeb && defaultTargetPlatform == TargetPlatform.windows) {
final availableVersion = await WebViewEnvironment.getAvailableVersion();
assert(availableVersion != null,
'Failed to find an installed WebView2 Runtime or non-stable Microsoft Edge installation.');

webViewEnvironment = await WebViewEnvironment.create(
settings: WebViewEnvironmentSettings(userDataFolder: 'YOUR_CUSTOM_PATH'));
}

if (!kIsWeb && defaultTargetPlatform == TargetPlatform.android) {
await InAppWebViewController.setWebContentsDebuggingEnabled(kDebugMode);
}

runApp(const MaterialApp(home: MyApp()));
}

class MyApp extends StatefulWidget {
const MyApp({super.key});


State<MyApp> createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
final GlobalKey webViewKey = GlobalKey();

InAppWebViewController? webViewController;
InAppWebViewSettings settings = InAppWebViewSettings(
isInspectable: kDebugMode,
mediaPlaybackRequiresUserGesture: false,
allowsInlineMediaPlayback: true,
iframeAllow: "camera; microphone",
iframeAllowFullscreen: true);

PullToRefreshController? pullToRefreshController;
String url = "";
double progress = 0;
final urlController = TextEditingController();


void initState() {
super.initState();

pullToRefreshController = kIsWeb ||
![TargetPlatform.iOS, TargetPlatform.android]
.contains(defaultTargetPlatform)
? null
: PullToRefreshController(
settings: PullToRefreshSettings(
color: Colors.blue,
),
onRefresh: () async {
if (defaultTargetPlatform == TargetPlatform.android) {
webViewController?.reload();
} else if (defaultTargetPlatform == TargetPlatform.iOS) {
webViewController?.loadUrl(
urlRequest:
URLRequest(url: await webViewController?.getUrl()));
}
},
);
}


Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text("Official InAppWebView website")),
body: SafeArea(
child: Column(children: <Widget>[
TextField(
decoration: const InputDecoration(prefixIcon: Icon(Icons.search)),
controller: urlController,
keyboardType: TextInputType.url,
onSubmitted: (value) {
var url = WebUri(value);
if (url.scheme.isEmpty) {
url = WebUri("https://www.google.com/search?q=$value");
}
webViewController?.loadUrl(urlRequest: URLRequest(url: url));
},
),
Expanded(
child: Stack(
children: [
InAppWebView(
key: webViewKey,
webViewEnvironment: webViewEnvironment,
initialUrlRequest:
URLRequest(url: WebUri("https://inappwebview.dev/")),
initialSettings: settings,
pullToRefreshController: pullToRefreshController,
onWebViewCreated: (controller) {
webViewController = controller;
},
onLoadStart: (controller, url) {
setState(() {
this.url = url.toString();
urlController.text = this.url;
});
},
onPermissionRequest: (controller, request) async {
return PermissionResponse(
resources: request.resources,
action: PermissionResponseAction.GRANT);
},
shouldOverrideUrlLoading:
(controller, navigationAction) async {
var uri = navigationAction.request.url!;

if (![
"http",
"https",
"file",
"chrome",
"data",
"javascript",
"about"
].contains(uri.scheme)) {
if (await canLaunchUrl(uri)) {
// Launch the App
await launchUrl(
uri,
);
// and cancel the request
return NavigationActionPolicy.CANCEL;
}
}

return NavigationActionPolicy.ALLOW;
},
onLoadStop: (controller, url) async {
pullToRefreshController?.endRefreshing();
setState(() {
this.url = url.toString();
urlController.text = this.url;
});
},
onReceivedError: (controller, request, error) {
pullToRefreshController?.endRefreshing();
},
onProgressChanged: (controller, progress) {
if (progress == 100) {
pullToRefreshController?.endRefreshing();
}
setState(() {
this.progress = progress / 100;
urlController.text = url;
});
},
onUpdateVisitedHistory: (controller, url, androidIsReload) {
setState(() {
this.url = url.toString();
urlController.text = this.url;
});
},
onConsoleMessage: (controller, consoleMessage) {
if (kDebugMode) {
print(consoleMessage);
}
},
),
progress < 1.0
? LinearProgressIndicator(value: progress)
: Container(),
],
),
),
ButtonBar(
alignment: MainAxisAlignment.center,
children: <Widget>[
ElevatedButton(
child: const Icon(Icons.arrow_back),
onPressed: () {
webViewController?.goBack();
},
),
ElevatedButton(
child: const Icon(Icons.arrow_forward),
onPressed: () {
webViewController?.goForward();
},
),
ElevatedButton(
child: const Icon(Icons.refresh),
onPressed: () {
webViewController?.reload();
},
),
],
),
])));
}
}

This is the result:

InAppWebView Android basic usage

Use JavaScript in WebView

JavaScript support is enabled by default. To disable it, set InAppWebViewSettings.javaScriptEnabled to false.

In general, calling methods that evaluates javascript code (such as evaluateJavascript) too early (for example inside onWebViewCreated or onLoadStart events), won't work because the WebView is not ready to handle it yet.

Instead, you should call these methods, for example, inside the onLoadStop event or in any other events where you know the page is ready "enough".

Check JavaScript docs for more details.

Handle page navigation

The basic navigation events are onLoadStart and on onLoadStop. Also, the onPageCommitVisible event is fired when the web view begins to receive web content.

caution

The onLoadStop event could be called multiple times. That's platform-specific, and it's related on how the native platform implements the WebView!

The onLoadStart and onLoadStop events are not called when the navigation state of the WebView changes through the usage of javascript, without a complete reload of the web page.

To detect these cases, use the onUpdateVisitedHistory event, that is an event fired when the host application updates its visited links database or when the navigation state of the WebView changes through the usage of javascript History API functions (pushState(), replaceState()) and onpopstate event or, also, when the javascript window.location changes without reloading the webview (for example appending or modifying a hash to the url).

Instead, to detect HTTP errors or loading errors, use onReceivedHttpError or onReceivedError respectively.

To cancel or allow the navigation requests, you should use the navigation event shouldOverrideUrlLoading, which gives the host application a chance to take control when a URL is about to be loaded in the current WebView using NavigationActionPolicy. To be able to listen this event, you must set InAppWebViewSettings.useShouldOverrideUrlLoading to true.

shouldOverrideUrlLoading NOTE for Android

Note that on Android there isn't any way to load an URL for a frame that is not the main frame, so if the request is not for the main frame, the navigation is allowed by default. However, if you want to cancel requests for subframes, you can use the InAppWebViewSettings.regexToCancelSubFramesLoading setting to write a Regular Expression that, if the url request of a subframe matches, then the request of that subframe is canceled.

Also, on Android, this event is not called for POST requests.

On Android, you can also use the shouldInterceptRequest event, that is an event that notifies the host application of a resource request and allow the application to return the data. If the return value is null, the WebView will continue to load the resource as usual. Otherwise, the return response and data will be used. To be able to listen this event, you must set InAppWebViewSettings.useShouldInterceptRequest to true.

This event is invoked for a variety of URL schemes (e.g., http(s):, data:, file:, etc.), not only those schemes which send requests over the network. This is not called for javascript: URLs, blob: URLs, or for assets accessed via file:///android_asset/ or file:///android_res/ URLs.

In the case of redirects, this is only called for the initial resource URL, not any subsequent redirect URLs.

Handle custom URLs

WebView applies restrictions when requesting resources and resolving links that use a custom URL scheme. For example, if you implement events such as shouldOverrideUrlLoading or shouldInterceptRequest, then WebView invokes them only for valid URLs.

For example, WebView may not call your shouldOverrideUrlLoading event for links like this:

<a href="showProfile">Show Profile</a>

Invalid URLs like above are handled inconsistently in WebView, so we recommend using a well-formed URL instead, such as using a custom scheme or using an HTTPS URL for a domain that your organization controls.

Instead of using a simple string in a link as shown earlier, you can use a custom scheme such as the following:

<a href="example-app:showProfile">Show Profile</a>

You can then handle this URL in your shouldOverrideUrlLoading event like this:

static final String APP_SCHEME = "example-app:";
// ...
InAppWebView(
shouldOverrideUrlLoading: (controller, navigationAction) async {
final uri = navigationAction.request.url;
if (uri != null && uri.toString().startsWith(APP_SCHEME)) {
// do whatever you want and cancel the request.
return NavigationActionPolicy.CANCEL;
}
return NavigationActionPolicy.ALLOW;
},
)

The shouldOverrideUrlLoading API is primarily intended for launching intents for specific URLs. When implementing it, make sure to return NavigationActionPolicy.ALLOW for URLs the WebView should handle. You're not limited to launching intents, though; you can replace launching intents with any custom behavior in the preceding code samples.

Handle custom schemes

Instead, if you want to handle custom schemes and return a custom response for that specific request, you can use the onLoadResourceWithCustomScheme event. With that event, you can handle the url request and return a CustomSchemeResponse to load a specific resource encoded to base64.

For all the platform except for Windows, to register the list of custom schemes you want to capture, set the InAppWebViewSettings.resourceCustomSchemes with a list of string values.

Instead, for Windows platform, you need to set up the WebViewEnvironment.customSchemeRegistrations parameter that accepts a list of CustomSchemeRegistration instances, for example:

webViewEnvironment = await WebViewEnvironment.create(
settings: WebViewEnvironmentSettings(
userDataFolder: 'YOUR_CUSTOM_PATH',
customSchemeRegistrations: [
CustomSchemeRegistration(
scheme: "my-special-custom-scheme",
allowedOrigins: ['*'],
treatAsSecure: true,
hasAuthorityComponent: true)
]));

Example:

import 'package:flutter/services.dart';

WebViewEnvironment? webViewEnvironment;

Future main() async {

//...

if (!kIsWeb && defaultTargetPlatform == TargetPlatform.windows) {
final availableVersion = await WebViewEnvironment.getAvailableVersion();
assert(availableVersion != null,
'Failed to find an installed WebView2 Runtime or non-stable Microsoft Edge installation.');

webViewEnvironment = await WebViewEnvironment.create(
settings: WebViewEnvironmentSettings(
userDataFolder: 'YOUR_CUSTOM_PATH',
customSchemeRegistrations: [
CustomSchemeRegistration(
scheme: "my-special-custom-scheme",
allowedOrigins: ['*'],
treatAsSecure: true,
hasAuthorityComponent: true)
]));
}

//...
}

//...

InAppWebView(
webViewEnvironment: webViewEnvironment,
initialUrlRequest: URLRequest(url: WebUri('https://example.com')),
initialSettings: InAppWebViewSettings(resourceCustomSchemes: ["my-special-custom-scheme"]),
onLoadResourceWithCustomScheme: (controller, request) async {
if (request.url.scheme == "my-special-custom-scheme") {
final bytes = await rootBundle.load(
"assets/${request.url.toString().replaceFirst("my-special-custom-scheme://", "", 0)}");
final response = CustomSchemeResponse(
data: bytes.buffer.asUint8List(),
contentType: "image/svg+xml",
contentEncoding: "utf-8");
return response;
}
return null;
},
),

When your WebView overrides URL loading, it automatically accumulates a history of visited web pages. You can navigate backward and forward through the history with InAppWebViewController.goBack(), InAppWebViewController.goForward(), and InAppWebViewController.goBackOrForward().

The InAppWebViewController.canGoBack() method returns true if there is actually web page history for the user to visit. Likewise, you can use InAppWebViewController.canGoForward() to check whether there is a forward history. If you don't perform this check, then once the user reaches the end of the history, InAppWebViewController.goBack(), InAppWebViewController.goForward(), or InAppWebViewController.goBackOrForward() does nothing.

To get the WebView history, use the InAppWebViewController.getCopyBackForwardList method, which returns the list of visited URLs and titles during the current session.

Manage windows

By default, requests to open new windows are ignored. This is true whether they are opened by JavaScript or by the target attribute in a link. You can customize this behaviour implementing onCreateWindow event.

info

To allow JavaScript to open windows, you need to set InAppWebViewSettings.javaScriptCanOpenWindowsAutomatically setting to true.

NOTE for Android

You need to set InAppWebViewSettings.supportMultipleWindows setting to true.

caution

To keep your app more secure, it's best to prevent popups and new windows from opening. The safest way to implement this behavior is to set InAppWebViewSettings.supportMultipleWindows setting to true but not implement onCreateWindow or simply return false. Note that this logic prevents any page that uses target="_blank" in its links from loading.

Instead, to detect when a WebView or a window should be closed and removed from the view system, you should implement the onCloseWindow event. At this point, WebCore has stopped any loading in this window and has removed any cross-scripting ability in javascript.

Use the onWindowFocus and onWindowBlur events to detect when a WebView has received or lost focus respectively.

Load local content

You can provide web-based content—such as HTML, JavaScript, and CSS—for your app to use that you statically compile into the application rather than fetch over the internet.

In-app content doesn't require internet access or consume a user's bandwidth, and if the content is designed specifically for WebView-only—that is, it depends on communicating with a native app—then users can't accidentally load it in a web browser.

However, there are some drawbacks to in-app content. Updating web-based content requires shipping a new app update, and there is the possibility of mismatched content between what's on a web site and what's in the app on your device if users have outdated app versions.

Android WebView Asset Loader

On Android, you can use WebViewAssetLoader, that is a flexible and performant way to load in-app content in a WebView object. This class supports:

  • Loading content with an HTTP(S) URL for compatibility with the same-origin policy.
  • Loading subresources such as JavaScript, CSS, images, and iframes.

Check WebView Asset Loader docs for more details

In-App Localhost Server

The InAppLocalhostServer allows you to create a simple server on http://localhost:[port]/ in order to be able to load your assets file on a local server.

Check In-App Localhost Server docs for more details

loadData

When your app only needs to load an HTML page and doesn't need to intercept subresources, consider using loadData(), which doesn't require application assets. You can use it as shown in the following code sample:

final html = """<html><body><p>Hello world</p></body></html>""";
final baseUrl = WebUri("https://example.com/");

webViewController.loadData(data: html, baseUrl: baseUrl, historyUrl: baseUrl);

Choose argument values carefully:

  • baseUrl: This is the URL your HTML content will be loaded as. This must be an HTTP(S) URL.
  • data: This is the HTML content you want to display, as a string.
  • mimeType: The default value is text/html.
  • encoding: The default value is utf8. On Android, this is unused when baseUrl is an HTTP(S) URL.
  • historyUrl: available only on Android. It represents the URL to use as the history entry.
  • allowingReadAccessTo: available only on iOS. Used in combination with baseUrl (using the file:// scheme), it represents the URL from which to read the web content.

It is strongly recommend using an HTTP(S) URL as the baseUrl, as this ensures your app complies with the same-origin policy.

Antipatterns

There are several other ways to load in-app content, but we strongly recommend against them:

  • file:// URLs and data: URLs are considered to be opaque origins, meaning that they can't take advantage of powerful web APIs such as fetch() or XMLHttpRequest.

  • Although InAppWebViewSettings.allowFileAccessFromFileURLs and InAppWebViewSettings.allowUniversalAccessFromFileURLs can work around the issues with file:// URLs, it is recommended against setting these to true because it leaves your app vulnerable to file-based exploits. It is recommended explicitly setting these to false on all API levels for the strongest security.

  • For the same reasons, it is recommended against file://android_assets/ and file://android_res/ URLs. The WebView Asset Loader AssetsPathHandler and ResourcesPathHandler classes are meant to be drop-in replacements.

  • Avoid using MixedContentMode.MIXED_CONTENT_ALWAYS_ALLOW. This setting generally is not necessary and weakens the security of your app. It is recommended loading your in-app content over the same scheme (HTTP or HTTPS) as your website's resources and using either MixedContentMode.MIXED_CONTENT_COMPATIBILITY_MODE or MixedContentMode.MIXED_CONTENT_NEVER_ALLOW, as appropriate.


Did you find it useful? Consider making a donation to support this project and leave a star on GitHub GitHub stars. Your support is really appreciated!