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:
- Android: AndroidViewSurface
- iOS: UiKitView
- macOS: AppKitView
- Windows: custom implementation
- Web: HtmlElementView
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.
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.
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:
- Android
- iOS
- macOS
- Windows
- Web
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.
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 AndroidNote 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;
},
),
Navigate web page history
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.
To allow JavaScript to open windows, you need to set InAppWebViewSettings.javaScriptCanOpenWindowsAutomatically
setting to true
.
You need to set InAppWebViewSettings.supportMultipleWindows
setting to true
.
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 istext/html
.encoding
: The default value isutf8
. 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 thefile://
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 anddata:
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
andInAppWebViewSettings.allowUniversalAccessFromFileURLs
can work around the issues withfile://
URLs, it is recommended against setting these totrue
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/
andfile://android_res/
URLs. The WebView Asset LoaderAssetsPathHandler
andResourcesPathHandler
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 eitherMixedContentMode.MIXED_CONTENT_COMPATIBILITY_MODE
orMixedContentMode.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 . Your support is really appreciated!