In-App-Webview-Flutter

in_app_webview_flutter (In-App-Webview-Flutter) is a maintained pub.dev package for embedding an inline WebView, running a headless WebView, and opening an in-app browser — updated for current Flutter/Dart, including Swift Package Manager-ready iOS/macOS platform deps, and without the upstream debug donation console banner.

This package is based on flutter_inappwebview (Apache-2.0) by Lorenzo Pichilli. Pub.dev package names cannot contain hyphens, so the published name is in_app_webview_flutter.

flutter_inappwebview_fx is unlisted. Use this package instead.

Why this package

  • Works with Flutter ≥ 3.32 / Dart ≥ 3.8 (tested on Flutter 3.47 / Dart 3.13)
  • Pulls platform implementations that include Swift Package Manager support (avoids the flutter_inappwebview_ios SPM warning on modern Flutter)
  • Keeps the familiar public API (InAppWebView, HeadlessInAppWebView, InAppBrowser, ChromeSafariBrowser, cookie manager, etc.)
  • Adds Linux support via upstream platform packages

Requirements

  • Dart SDK: ^3.8.0
  • Flutter: >=3.32.0
  • Android: minSdkVersion >= 19, AGP >= 7.3.0
  • iOS 12.0+, Xcode >= 15.0 (SPM-capable platform package)
  • macOS 10.14+, Xcode >= 15.0
  • Windows: NuGet CLI on PATH
  • Linux: WPE 2.0 WebKit

Installation

dependencies:
  in_app_webview_flutter: ^1.0.0
import 'package:in_app_webview_flutter/flutter_inappwebview.dart';

Migration from flutter_inappwebview or flutter_inappwebview_fx: only the package/import name changes. Widget and class names stay the same.

import 'package:in_app_webview_flutter/flutter_inappwebview.dart';
// or: import 'package:in_app_webview_flutter/in_app_webview_flutter.dart';

Versioning: this package starts at 1.0.0. It is based on upstream flutter_inappwebview 6.2.0-beta.3 — see the CHANGELOG for the upstream base of each release.

Quick example

import 'package:flutter/material.dart';
import 'package:in_app_webview_flutter/flutter_inappwebview.dart';

class SimpleWebView extends StatelessWidget {
  const SimpleWebView({super.key});

  @override
  Widget build(BuildContext context) {
    return InAppWebView(
      initialUrlRequest: URLRequest(url: WebUri('https://flutter.dev')),
    );
  }
}

App shell: making a page feel native

InAppWebViewFx is an opt-in wrapper around InAppWebView that applies a declarative WebAppShell. Plain InAppWebView is untouched, so adopting it is a drop-in change on a per-screen basis.

InAppWebViewFx(
  initialUrlRequest: URLRequest(url: WebUri('https://shop.example.com')),
  shell: WebAppShell(
    // Hide the site's own header, nav and footer — Flutter supplies the chrome.
    // Best-effort for classic HTML. Component SPAs usually need the picker.
    hide: ElementHiding.preset(HidePreset.siteChrome)
        .plusSelectors(['.newsletter-popup']),
    // Body text unselectable, no long-press callout, forms still work.
    selection: WebSelection.appLike,
    // No white flash on load; page renders in the app's light/dark variant.
    theme: WebTheme.of(context),
    // Keep sticky bars clear of the home indicator.
    safeArea: WebSafeArea.bottomOnly,
    // Off by default — leave it off for third-party SPAs.
    reportUnmatchedSelectors: false,
  ),
)

What the shell handles:

  • Element hiding. Rules are enforced both as a native css-display-none content blocker (applied before first paint, and unaffected by a strict style-src CSP) and as an injected stylesheet that is re-asserted on DOM mutations and history.pushState route changes, so single-page apps cannot bring hidden elements back. HidePreset.siteChrome targets classic header / nav / footer landmarks — React/Vue chrome often will not match. The CSS4 i flag ([class*="Navbar" i]) works in the stylesheet path; native content blockers drop those selectors so one invalid flag cannot reject the whole rule list.
  • Text selection. WebSelection.appLike, .noCallout and .disabled. Editable elements are always exempt, so disabling selection does not break the site's forms.
  • Theme integration. A backgroundColor makes the WebView transparent and paints Flutter's colour underneath, removing the white flash during load, and color-scheme is injected so the page picks its own dark variant. forceDarkFallback: true inverts pages that have no dark theme at all, re-inverting images and video.
  • Safe areas. env(safe-area-inset-*) reads as zero inside an embedded WebView, so Flutter's real insets are published as --fx-safe-top / --fx-safe-right / --fx-safe-bottom / --fx-safe-left custom properties, which first-party page CSS can also consume directly.
  • Error pages. The platform error page is suppressed and replaced with a themed Flutter overlay offering a retry. Pass WebErrorHandling(builder: ...) to supply your own.

Finding selectors

Hard-coding selectors for a site you do not control is the fragile part, so the shell helps on both ends. Turn on the element picker in a debug build and tap whatever you want gone:

InAppWebViewFx(
  elementPickerEnabled: kDebugMode,
  onElementPicked: (controller, element) => debugPrint(element.selector),
  shell: shell,
)

And when a site's markup later drifts, selectors that stop matching are reported rather than failing silently:

onUnmatchedSelectors: (controller, selectors, url) =>
    debugPrint('stale hide rules on $url: $selectors'),

Reporting is off by default. When you turn it on (only while writing rules), the same miss list is reported at most once per URL — SPA pushState / hash changes and shell reinject do not flood the JS bridge. Use WebAppShell(reportUnmatchedSelectors: false) for third-party SPAs.

Reshaping a site you do not own means its markup can change under you, and hiding third-party content may carry terms-of-service implications. Both are worth checking before shipping.

Embedding a third-party SPA (Flutter owns the tabs)

When a Flutter NavigationBar drives a React/Vue site, do not pad the page again and do not assume HidePreset.siteChrome matches:

InAppWebViewFx(
  initialUrlRequest: URLRequest(url: WebUri('https://example.com')),
  initialSettings: InAppWebViewSettings(
    useShouldOverrideUrlLoading: true,
  ),
  initialUserScripts: UnmodifiableListView([
    UserScript(
      groupName: 'app.probe',
      source: 'if (!window.__appProbe) window.__appProbe = true;',
      injectionTime: UserScriptInjectionTime.AT_DOCUMENT_START,
    ),
  ]),
  shell: WebAppShell.appEmbedded(
    theme: WebTheme.of(context),
    // WebSafeArea.variablesOnly — Flutter's bar already owns the home indicator.
    reportUnmatchedSelectors: false,
  ),
  onReceivedServerTrustAuthRequest: (controller, challenge) async {
    return ServerTrustAuthResponse(
      action: ServerTrustAuthResponseAction.PROCEED,
    );
  },
  onUpdateVisitedHistory: (controller, url, isReload) {
    // Sync the selected Flutter tab from url.path.
  },
  shouldOverrideUrlLoading: (controller, action) async {
    final scheme = action.request.url?.scheme;
    if (scheme == 'http' || scheme == 'https') {
      return NavigationActionPolicy.ALLOW;
    }
    return NavigationActionPolicy.CANCEL; // tel:, mailto:, custom schemes
  },
)

InAppWebViewFx forwards production callbacks (onReceivedServerTrustAuthRequest, onJsAlert / onJsConfirm / onJsPrompt, onReceivedHttpAuthRequest, onReceivedLoginRequest, shouldInterceptRequest, …). Host UserScripts are re-evaluated after a full document load (they must be idempotent); the shell group is never removed to do this. Same-document SPA navigations are handled in-page — Dart does not re-inject on every history tick.

Optional WebAppShell(diagnostics: WebViewDiagnostics.debug) emits one [in_app_webview_flutter][perf] /path total=… line per full load.

The example app has two screens: App Shell (toggles) and Embedded SPA (/app-embedded-spa) for this Flutter-nav + third-party site pattern.

For API corners the wrapper does not expose (deprecated Android/iOS-prefixed aliases), drive the shell against a plain InAppWebView with WebAppShell.applyToSettings, WebAppShell.buildUserScript, WebAppShell.inject and WebAppShell.reapplyUserScripts.

Supported platforms

Android · iOS · macOS · Windows · Linux · Web (via endorsed federated implementations)

Docs & upstream

License

Apache License 2.0 — see LICENSE. Copyright remains with the original authors; this package is an independently published fork for pub.dev naming and Flutter currency.