webview_flutter_windows 1.0.0
webview_flutter_windows: ^1.0.0 copied to clipboard
A WebView2-powered webview for Flutter on Windows with seamless composition, keyboard focus integration, and a typed event API.
webview_flutter_windows #
A WebView for Flutter on Windows, powered by
Microsoft Edge WebView2.
The browser is rendered off-screen and composited into your widget tree as a
regular Flutter Texture, so it behaves like any other widget: no airspace
issues, and transforms, opacity, and widgets painted on top all just work.
Fork notice: this is a maintained fork of jnschulze/flutter-webview-windows. It is not affiliated with the Flutter team's
webview_flutterpackages and keeps the controller/widget API fromwebview_windows. On top of upstream0.4.0it fixes the window focus loss issue (#230), hardens the native COM and channel layers, modernizes the toolchain (WebView2 SDK1.0.3967.48, WIL1.0.260126.7, C++23), and ships a real test suite. Coming from upstream? Read the migration guide.

Features #
- Seamless composition. Web content renders into a Flutter
Textureinside your widget tree, not a native window floating above it. - Keyboard focus that works. Clicking the webview does not deactivate
your window, clicking back on Flutter UI restores Flutter's keyboard
handling instantly, and
Tabtraversal leaves the page cleanly. - Typed broadcast event streams for URL, loading state, document title, navigation history, security state, full-screen elements, downloads, load errors, web messages, and native focus.
- Two-way JavaScript bridge: execute scripts, register on-document-created scripts, and exchange JSON messages with the page.
- Browser control: full cookie management (read, write, delete), cache, user agent, zoom, background color, popup policy, virtual host mapping, suspend/resume, FPS limiting, and DevTools.
- Headless mode: drive a controller without a widget for background pages, scraping, or pre-warming.
- Faithful input forwarding: mouse, high-precision trackpad scrolling, and multi-touch.
- High-DPI aware, including per-view scale factors in multi-window apps.
Requirements #
On your users' machines
- Windows 10 1809 or newer (the off-screen compositor relies on
Windows.Graphics.Capture). - The WebView2 Runtime.
It ships with Windows 11 and current Windows 10. To verify at startup, call
WebviewController.getWebViewVersion(): if it returnsnull, guide the user to install the runtime.
On your development machine
- Flutter 3.44+ / Dart 3.12+
- Visual Studio 2022 with the Desktop development with C++ workload
- A Windows 10/11 SDK (installed with the workload by default)
- Optional:
nuget.exein yourPATH(the build downloads it automatically if missing)
Installation #
flutter pub add webview_flutter_windows
To track unreleased changes, depend on the repository instead:
dependencies:
webview_flutter_windows:
git:
url: https://github.com/omar-hanafy/webview_flutter_windows.git
ref: main
Quick start #
Create a WebviewController, initialize it, and hand it to a Webview
widget:
import 'package:flutter/material.dart';
import 'package:webview_flutter_windows/webview_flutter_windows.dart';
class BrowserPane extends StatefulWidget {
const BrowserPane({super.key});
@override
State<BrowserPane> createState() => _BrowserPaneState();
}
class _BrowserPaneState extends State<BrowserPane> {
final _controller = WebviewController();
@override
void initState() {
super.initState();
_initWebview();
}
Future<void> _initWebview() async {
await _controller.initialize();
await _controller.setPopupWindowPolicy(WebviewPopupWindowPolicy.deny);
await _controller.loadUrl('https://flutter.dev');
if (mounted) {
setState(() {});
}
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return _controller.value.isInitialized
? Webview(_controller)
: const Center(child: CircularProgressIndicator());
}
}
initialize() completes with an error if the WebView2 Runtime is missing
(error code environment_creation_failed), so wrap it in a try/catch if
you want to show a friendly install prompt.
Listening to events #
All controller streams are broadcast streams: attach as many listeners as you like, but subscribe before triggering navigation, because events emitted while nobody listens are dropped.
controller.url.listen((url) => debugPrint('URL: $url'));
controller.loadingState.listen((state) => debugPrint('State: $state'));
controller.title.listen((title) => debugPrint('Title: $title'));
controller.historyChanged.listen((h) => debugPrint('canGoBack: ${h.canGoBack}'));
controller.onLoadError.listen((status) => debugPrint('Load error: $status'));
controller.onDownloadEvent.listen((e) => debugPrint('${e.kind}: ${e.url}'));
Talking to the page #
Execute JavaScript and get its (JSON-decoded) result, or inject scripts that run before any page script:
final title = await controller.executeScript('document.title');
final scriptId = await controller.addScriptToExecuteOnDocumentCreated(
'window.myAppReady = true;',
);
Exchange structured messages with the page:
// Dart -> page
await controller.postWebMessage('{"command": "ping"}');
// page -> Dart
controller.webMessage.listen((message) {
debugPrint('Page says: $message'); // already JSON-decoded
});
And on the JavaScript side:
window.chrome.webview.addEventListener('message', (e) => {
console.log(e.data); // {command: 'ping'}
});
window.chrome.webview.postMessage({command: 'pong'});
Cookies #
Read, write, and delete cookies through the WebView2 cookie store - useful for extracting a session token after a login flow or seeding one before it:
final cookies = await controller.getCookies('https://example.com');
final session = cookies.firstWhere((c) => c.name == 'session');
await controller.setCookie(WebviewCookie(
name: 'theme',
value: 'dark',
domain: '.example.com',
expires: DateTime.now().add(const Duration(days: 30)),
isSecure: true,
sameSite: WebviewCookieSameSite.strict,
));
await controller.deleteCookies('theme', uri: 'https://example.com');
await controller.clearCookies(); // everything
Omit expires to create a session cookie; pass an empty uri to
getCookies to list every cookie of the profile.
Headless usage #
A controller works without a Webview widget: initialize it, give the
invisible page real bounds with setSize (pages do not perform layout until
they have nonzero bounds), and drive it through the normal API - load pages,
run scripts, exchange messages, read cookies. Nothing is rendered and no
frames are captured:
final background = WebviewController();
await background.initialize();
await background.setSize(const Size(1280, 720));
await background.loadUrl('https://example.com/login');
// ... executeScript, webMessage, getCookies ...
await background.dispose();
Permission requests #
Web content can request browser permissions (camera, microphone, geolocation,
clipboard, notifications). Decide per request via the Webview widget:
Webview(
controller,
permissionRequested: (url, kind, isUserInitiated) async {
final allowed = await askTheUser(url, kind);
return allowed
? WebviewPermissionDecision.allow
: WebviewPermissionDecision.deny;
},
)
Return WebviewPermissionDecision.none to defer to the WebView2 default.
Keyboard focus #
Clicking into the webview gives the page real Win32 keyboard focus; clicking
any Flutter widget hands focus back automatically. On top of that, one
invariant is enforced in both directions: while a Flutter text input owns
Flutter focus, no webview keeps native keyboard focus. If a dialog's
TextField gains focus while the page has the keyboard, focus is handed
back; if anything - focus(), page script, a stale refocus path in app
code - grabs native focus while a text input is focused, the grab is
reverted immediately. Typing can never silently land in the page. To move
focus into the page programmatically, unfocus the text input first. You
normally do not have to manage any of this. For programmatic control:
await controller.focus(); // move keyboard focus into the page
await WebviewController.releaseFocus(); // hand it back to Flutter
controller.onFocusChanged.listen((focused) => debugPrint('Page focus: $focused'));
final focused = controller.hasNativeFocus;
Configuring the browser environment #
The WebView2 environment is shared by all controllers and is created lazily
on the first initialize(). To customize it (user data directory, browser
executable, Chromium command line flags), configure it once before creating
any controller:
await WebviewController.initializeEnvironment(
userDataPath: r'C:\path\to\profile',
additionalArguments: '--show-fps-counter',
);
The environment's lifecycle is reference counted: it is released when the
last controller is disposed. That means initializeEnvironment can be
called again - for example with a different userDataPath - once all
controllers are gone; it only throws a PlatformException while controllers
are alive.
Example app and tests #
A complete example (URL bar, loading indicator, DevTools, permission prompts)
lives in example/: cd example && flutter run -d windows.
The repository also ships:
-
a Dart test suite (
flutter test) covering the channel contract of every controller method, the controller lifecycle, all events, and input/focus forwarding, -
native C++ unit tests (GoogleTest), run from the repository root:
cmake -S windows/test -B build/native_tests cmake --build build/native_tests --config Release ctest --test-dir build/native_tests -C Release --output-on-failure -
a real-input integration test for the focus behavior (
example/integration_test/focus_test.dart), which needs an interactive Windows session.
Tip for your own widget tests: when driving a WebviewController under
testWidgets, wrap initialize() and dispose() in tester.runAsync(...).
Test bodies run in a fake-async zone where platform channel futures are not
driven, so awaiting them directly hangs the test.
Limitations #
WebView2 has no official off-screen rendering API yet
(WebView2Feedback#20,
#526,
#547).
This plugin obtains frames through Windows.Graphics.Capture, which is why
Windows versions older than Windows 10 1809 are not supported.
Troubleshooting #
initialize()fails withenvironment_creation_failed: the WebView2 Runtime is missing. CheckWebviewController.getWebViewVersion()and point the user to the runtime installer.- The build cannot download NuGet: install
nuget.exe manually and add it to
PATH. - Keyboard input stays in the page after clicking it: that is by design;
click any Flutter widget or call
WebviewController.releaseFocus()to return focus to Flutter.
Migrating from upstream 0.4.x #
Version 1.0.0 contains breaking changes (SDK floors, broadcast streams,
WebErrorStatus renames, stricter lifecycle errors). The
migration guide
walks through every one of them, including the focus workarounds you can now
delete.
Credits and license #
Created by Niklas Schulze (jnschulze); this fork is maintained by Omar Hanafy. Licensed under the BSD 3-Clause License.