Embedding WebViews in Flutter
The payment provider shipped a hosted checkout page faster than we could build native PCI scope. We embedded it in flutter_inappwebview, wired a JS handler for paymentComplete, and closed the sheet when the bridge fired. The hard parts were cookie persistence, back-button behavior, and stopping the OAuth redirect from opening Safari instead of staying in-app.
webview_flutter basics
dependencies:
webview_flutter: ^4.10.0
late final WebViewController controller;
@override
void initState() {
super.initState();
controller = WebViewController()
..setJavaScriptMode(JavaScriptMode.unrestricted)
..loadRequest(Uri.parse('https://docs.example.com'));
}
@override
Widget build(BuildContext context) {
return WebViewWidget(controller: controller);
}
Sufficient for read-only docs with no bridge.
flutter_inappwebview for hybrid apps
dependencies:
flutter_inappwebview: ^6.1.0
InAppWebView(
initialUrlRequest: URLRequest(url: WebUri('https://app.example.com/checkout')),
initialSettings: InAppWebViewSettings(
javaScriptEnabled: true,
allowsInlineMediaPlayback: true,
),
onWebViewCreated: (webViewController) {
_controller = webViewController;
webViewController.addJavaScriptHandler(
handlerName: 'paymentComplete',
callback: (args) {
final orderId = args.first as String;
Navigator.pop(context, orderId);
},
);
},
shouldOverrideUrlLoading: (controller, action) async {
final url = action.request.url;
if (url?.host == 'evil.com') return NavigationActionPolicy.CANCEL;
return NavigationActionPolicy.ALLOW;
},
)
Page calls bridge:
window.flutter_inappwebview.callHandler('paymentComplete', orderId);
Cookies and auth
Share session cookies between native login and WebView:
final cookieManager = CookieManager.instance();
await cookieManager.setCookie(
url: WebUri('https://app.example.com'),
name: 'session',
value: token,
isSecure: true,
);
Verify SameSite and HttpOnly flags match server expectations—misconfigured cookies cause infinite login loops.
Navigation and back button
PopScope(
canPop: false,
onPopInvoked: (didPop) async {
if (didPop) return;
if (await _controller.canGoBack()) {
_controller.goBack();
} else if (context.mounted) {
Navigator.pop(context);
}
},
child: InAppWebView(...),
)
Android back must pop WebView history before closing route.
Performance
WebViews are heavy—avoid lists of many simultaneous instances. Prefer one full-screen WebView or load static native UI around it.
Warm up by preloading off-screen if UX requires instant display.
Clear cache on logout if pages cache sensitive data:
await InAppWebViewController.clearAllCache();
Security checklist
- Allowlist hosts in
shouldOverrideUrlLoading - Disable universal file access
- Do not enable JavaScript on untrusted content
- Pin SSL or use HTTPS only
- Sanitize data passed to
evaluateJavascript
OAuth and redirect handling
Identity providers love redirect chains. When Google OAuth returns to https://app.example.com/callback?code=..., the WebView must stay in-app—not hand off to Chrome. Intercept custom schemes and app links:
shouldOverrideUrlLoading: (controller, action) async {
final uri = action.request.url;
if (uri == null) return NavigationActionPolicy.CANCEL;
if (uri.scheme == 'myapp') {
ref.read(authNotifierProvider.notifier).handleCallback(uri);
return NavigationActionPolicy.CANCEL;
}
if (!_allowedHosts.contains(uri.host)) {
return NavigationActionPolicy.CANCEL;
}
return NavigationActionPolicy.ALLOW;
},
Register the same callback URL in your native deep link config so cold-start OAuth still works if the user leaves mid-flow. Test with provider sandbox accounts; staging OAuth client IDs often differ from production.
Injecting cookies before load helps SSO: set session cookie, then loadUrl to /dashboard instead of forcing another login screen inside the WebView.
User agent and desktop mode
Some sites serve broken mobile layouts inside WebView. Override user agent sparingly—payment gateways may reject non-standard agents:
initialSettings: InAppWebViewSettings(
userAgent: 'MyApp/2.1 (Flutter; Android) AppleWebKit/537.36',
),
Document the UA string with vendors before production. iOS WKWebView and Android WebView differ in JavaScript engine features; feature-detect in JS rather than assuming Chrome desktop APIs.
Testing hybrid flows
Widget tests struggle with real WebView—use integration tests on device for bridge contracts. Unit-test message parsing separately:
test('parses paymentComplete payload', () {
final orderId = parsePaymentArgs(['ord_123']);
expect(orderId, 'ord_123');
});
Maintain a stub HTML page in assets/test/ that fires handlers on button click for QA builds. Log bridge traffic in debug only; redact tokens before crash reports.
Platform differences worth planning for
Android allows multiple WebView implementations (Chrome WebView vs legacy). InAppWebView abstracts most differences, but file upload (<input type="file">) and geolocation permission prompts need platform-specific permission grants in AndroidManifest.xml and Info.plist.
iOS requires NSAppTransportSecurity exceptions only when absolutely necessary—prefer fixing server TLS. Android cleartext traffic blocked by default on newer API levels; do not load http:// in production WebViews.
Memory: dispose controllers when routes pop. iOS WKWebView processes can linger if pages hold large media buffers—call stopLoading() before dispose on checkout completion.
File upload and geolocation
Handle onPermissionRequest for camera/microphone inside WebView flows—delegate to app permission state:
onPermissionRequest: (controller, request) async {
return PermissionResponse(
resources: request.resources,
action: PermissionResponseAction.GRANT,
);
},
Grant only after app-level permission granted—do not auto-grant sensitive resources.
Desktop WebView
flutter_inappwebview supports desktop targets—test Windows/macOS if shipping hybrid desktop; cookie isolation differs from mobile.
SSL pinning interaction
If app pins SSL for native HTTP client, WebView may use separate trust store—configure WebView to respect pinning or exempt hosted pages consistently with security review.
incognito mode
Some flows require no cookie persistence—use separate WebView profile or clear all storage on close for guest checkout.
Print and PDF
Users may expect print from WebView content—platform print APIs differ; test share/print menu if contract requires.
Zoom and text scaling
Respect system font scale inside WebView where possible—some CSS fixes font-size preventing accessibility zoom; coordinate with web team.
Desktop cookie jar
Desktop WebView cookie persistence path differs—test login persistence on Windows/macOS targets if shipping Flutter desktop hybrid app to enterprise.
Resources
- flutter_inappwebview documentation
- webview_flutter package
- flutter_inappwebview pub.dev
- Android WebView security best practices
- Apple WKWebView documentation
Frequently asked questions
webview_flutter vs flutter_inappwebview?
webview_flutter is the official minimal plugin—fine for simple URL display. flutter_inappwebview adds JS handlers, cookie managers, interceptors, pull-to-refresh, and desktop support—choose it for production hybrid features.
How do I communicate between Flutter and JavaScript?
Register JavaScript handlers that Dart listens to, and call evaluateJavascript from Flutter to invoke page functions. Define a narrow JSON message protocol—never pass raw user input into eval strings.
Are WebViews a security risk?
Yes if they load untrusted URLs or expose JS bridges to arbitrary pages. Allowlist domains, disable file access, validate SSL, and avoid exposing native capabilities to untrusted JS.
Hiring a senior Android / Flutter engineer?
I architect and ship production mobile software — Kotlin, Jetpack Compose, Flutter — for robotics, EV infrastructure, fintech, and real-time systems. Open to remote roles in Europe and the US.
Get in touch →