With webview_flutter 4 in Flutter, how do you show a web page, and when would you switch its Android widget to hybrid composition?
answer
- WebViewController plus WebViewWidget
- loadRequest with a Uri
- setJavaScriptMode for script-heavy pages
- Android: texture layer by default
- displayWithHybridComposition on creation params
basics
~20 sCreate a WebViewController, configure it (for example setJavaScriptMode and loadRequest), and pass it to a WebViewWidget. On Android the widget uses texture layer composition by default; switch to hybrid composition through AndroidWebViewWidgetCreationParams when texture mode misbehaves.
solid answer
~30 sSince webview_flutter 4, configuration lives on a `WebViewController` and display on a `WebViewWidget(controller: ...)`. You typically call `setJavaScriptMode(JavaScriptMode.unrestricted)` if the page needs scripts, set a `NavigationDelegate` (for `onNavigationRequest`, `onPageFinished`, `onWebResourceError`), then `loadRequest(Uri.parse(...))`. The plugin is federated: Android uses `webview_flutter_android` and iOS/macOS `webview_flutter_wkwebview` (a `WKWebView`). On Android the widget is a `PlatformViewLink` with `AndroidViewSurface` created via `initSurfaceAndroidView`, i.e. texture layer hybrid composition. Setting `displayWithHybridComposition: true` on `AndroidWebViewWidgetCreationParams` switches it to `initExpensiveAndroidView` — useful when texture mode shows rendering or input problems, at the cost of Flutter frame rate.
go deeper
Know the pair WebViewController and WebViewWidget, and that loadRequest with a Uri shows a page.
Explain JavaScriptMode, NavigationDelegate callbacks and why the controller lives in State, plus the 4.0 migration away from the old WebView widget.
Decide when to switch Android to hybrid composition via AndroidWebViewWidgetCreationParams, measuring the frame-rate cost against rendering problems.
Judge when embedded web content is the right product choice versus native Flutter screens, considering security, navigation control and performance.
## The package at a glance `webview_flutter` shows web content inside a Flutter app by hosting the platform's own browser view as a **platform view**. It is a federated plugin: | Platform | Implementation package | Native view | |---|---|---| | Android | `webview_flutter_android` | Android `WebView` | | iOS and macOS | `webview_flutter_wkwebview` | `WKWebView` | The app depends on `webview_flutter`; the implementations come with it. ## The version 4 API Version 4.0 rewrote the API around a controller. The older `WebView` widget with `javascriptMode` and `javascriptChannels` parameters is gone: - `WebView.javascriptMode` became `WebViewController.setJavaScriptMode`; - JavaScript channels moved to `addJavaScriptChannel` / `removeJavaScriptChannel`; - `evaluateJavascript` was replaced by `runJavaScript` and `runJavaScriptReturningResult`. ```dart import 'package:flutter/material.dart'; import 'package:webview_flutter/webview_flutter.dart'; class ListingTermsPage extends StatefulWidget { const ListingTermsPage({super.key}); @override State<ListingTermsPage> createState() => _ListingTermsPageState(); } class _ListingTermsPageState extends State<ListingTermsPage> { late final WebViewController _controller = WebViewController() ..setJavaScriptMode(JavaScriptMode.unrestricted) ..setNavigationDelegate( NavigationDelegate( onNavigationRequest: (request) => request.url.startsWith('https://example.com/') ? NavigationDecision.navigate : NavigationDecision.prevent, ), ) ..loadRequest(Uri.parse('https://example.com/terms')); @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text('Terms')), body: WebViewWidget(controller: _controller), ); } } ``` Key points: 1. The controller is created once (here in `State`), not in `build()`, so rebuilds do not reload the page. 2. `JavaScriptMode` has two values, `disabled` and `unrestricted`; set it explicitly rather than relying on each platform's default, and choose `unrestricted` only for pages that need scripts. 3. `NavigationDelegate` callbacks let you allow or block navigations and react to load completion and errors. 4. Like any platform view, `WebViewWidget` needs bounded constraints and forwards `gestureRecognizers` (default: an empty set) for use inside scrollables. ## How it composes on Android `webview_flutter_android` builds a `PlatformViewLink` whose surface is an `AndroidViewSurface`, and creates the view with: - `PlatformViewsService.initSurfaceAndroidView` by default — **texture layer hybrid composition** (TLHC), with a fallback to hybrid composition when the view cannot be hosted as a texture; - `PlatformViewsService.initExpensiveAndroidView` when `displayWithHybridComposition` is `true` — always **hybrid composition**. The package documents that the flag should be `false` for most use cases: hybrid composition has performance costs, but it avoids the limitation of rendering to an Android `SurfaceTexture`. ```dart final params = AndroidWebViewWidgetCreationParams( controller: _controller.platform, displayWithHybridComposition: true, ); final widget = WebViewWidget.fromPlatformCreationParams(params: params); ``` Switch when you see texture-mode problems in real use — for example rendering glitches with fast-scrolling pages or content that relies on native surfaces — and measure the frame-rate cost afterwards. ## On iOS iOS platform views always use hybrid composition: the `WKWebView` is added to the native view hierarchy, so there is no mode switch to make. ## Common mistakes - Creating `WebViewController()` inside `build()`, which reloads the page on every rebuild. - Relying on platform defaults for JavaScript instead of calling `setJavaScriptMode`, then finding a script-heavy page inert on one platform. - Using pre-4.0 samples with `WebView(initialUrl: ...)`, which no longer compile. - Leaving navigation unrestricted, so a link inside the page can take users anywhere inside your app's web view.
- Why create the WebViewController in State rather than in build()?The controller owns the native web view's configuration and loaded page. Creating it in `build()` would make a new controller, and a new `loadRequest`, on every rebuild, reloading the page and losing its state.
- How do you stop the web view from navigating to an outside site?Set a `NavigationDelegate` with `onNavigationRequest` that returns `NavigationDecision.prevent` for URLs outside your allow-list and `NavigationDecision.navigate` otherwise; you can open blocked links in the system browser instead.
saying these in an interview costs you the question
- webview_flutter 4 still uses WebView(initialUrl: ...) as its main widget.
- JavaScript runs in the web view regardless of JavaScriptMode.
- displayWithHybridComposition should be true for every Android app.
- On iOS you can choose texture layer mode for the web view.
- Creating the controller inside build() is harmless.