skip to content

With webview_flutter 4 in Flutter, how do you show a web page, and when would you switch its Android widget to hybrid composition?

level: middleimportance: should knowfreq 44%

answer

  1. WebViewController plus WebViewWidget
  2. loadRequest with a Uri
  3. setJavaScriptMode for script-heavy pages
  4. Android: texture layer by default
  5. displayWithHybridComposition on creation params

basics

~20 s

Create 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 s

Since 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

for a junior

Know the pair WebViewController and WebViewWidget, and that loadRequest with a Uri shows a page.

for a middle

Explain JavaScriptMode, NavigationDelegate callbacks and why the controller lives in State, plus the 4.0 migration away from the old WebView widget.

for a senior

Decide when to switch Android to hybrid composition via AndroidWebViewWidgetCreationParams, measuring the frame-rate cost against rendering problems.

for a principal

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.