fixit_webview 0.11.0-beta.5
fixit_webview: ^0.11.0-beta.5 copied to clipboard
A production-grade Flutter WebView plugin with JS bridge, native Dart module integration, offline engine, theme engine, and enterprise-grade navigation controls.
Fixit WebView #
A production-grade Flutter WebView plugin with a JavaScript bridge, native Dart module integration, offline engine, theme engine, and enterprise navigation controls. Designed for wrapping websites as native mobile apps with rich Dart ↔ Web interaction.
Need priority support or custom integration? Contact: v02.aaditya@gmail.com
Table of Contents #
- Features
- Installation
- Quick Start (Complete App)
- Architecture
- Core Concepts
- JavaScript Bridge
- Navigation & Security
- Upload Engine
- Download Engine
- Offline Engine
- Theme Engine
- Permissions
- Cookies & Sessions
- Performance Diagnostics
- Force Update
- Crash Recovery & Splash
- Putting It All Together
- Troubleshooting & FAQ
- Best Practices
- API Reference
- Platform Support
- Examples
- Roadmap
- License
Features #
- WebView Runtime — Full-featured WebView backed by Android System WebView and iOS WKWebView
- JavaScript Bridge — Bidirectional JS ↔ Dart communication via
window.FixitBridgeand CustomEvent protocol - Navigation Controls — Whitelist/blacklist, deep links, URL routing, HTTP auth, SSL error handling
- Offline Engine — Cache-first and network-first strategies with disk persistence and retry queue
- Upload Engine — File upload from web forms with camera/gallery capture and multi-file support
- Download Engine — File download with progress tracking and system download manager integration
- Theme Engine — Auto light/dark CSS injection via
<style>element, reactive to system brightness - Crash Recovery — Automatic WebView restoration after renderer crash with custom overlay UI
- Splash → WebView Transition — Animated fade with configurable duration (white flash prevention)
- Custom Loader — Overlay with progress during page navigation
- Force Update — Remote version check with blocking or dismissible update dialog
- Pull-to-Refresh — Native
RefreshIndicatorintegration - Permissions API — Camera, microphone, and location permission check/request
- OAuth Interceptor — Automatic detection and parsing of OAuth callback URLs
- Cookie & Session Management — Persistent cookie store with disk backup
- Memory Pressure Handling — OS-level detection with auto cache clearing
- Lifecycle Management — Automatic pause/resume, cookie flushing, focus restoration
- Pooling — WebView instance pooling for faster cold starts
- Performance Diagnostics — Startup timeline (T0–T5), FPS, milestone export
Installation #
Add to your pubspec.yaml:
dependencies:
fixit_webview: ^0.11.0-beta.5
fixit_core: ^1.0.1
Then run:
flutter pub get
Note:
fixit_coreis a required peer dependency providing logging, caching, session, and event bus infrastructure. You must add it to yourpubspec.yamleven thoughfixit_webviewdepends on it transitively.
Android Setup #
No additional setup is required. The plugin uses Android System WebView (API 26+).
Add this permission to android/app/src/main/AndroidManifest.xml if you need network state detection:
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
iOS Setup #
Target iOS 15.0+ in your ios/Podfile:
platform :ios, '15.0'
❤️ Love fixit_webview? #
If this package saved you hours of development and helped your project, consider supporting future updates.
Quick Start (Complete App) #
Here is a complete, copy-pasteable Flutter app that demonstrates:
- WebView initialization with runtime
- JS ↔ Dart bridge with a custom handler
- Navigation controls (back, forward, reload)
- URL loading from a text field
- Lifecycle management
1. Full App Code #
main.dart:
import 'package:flutter/material.dart';
import 'package:fixit_core/fixit_core.dart';
import 'package:fixit_webview/fixit_webview.dart';
void main() => runApp(const MyApp());
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
title: 'Fixit WebView',
theme: ThemeData(colorSchemeSeed: Colors.indigo),
home: const WebViewScreen(),
);
}
}
class WebViewScreen extends StatefulWidget {
const WebViewScreen({super.key});
@override
State<WebViewScreen> createState() => _WebViewScreenState();
}
class _WebViewScreenState extends State<WebViewScreen> {
late final FixitRuntime _runtime;
late final FixitWebViewController _controller;
bool _ready = false;
final TextEditingController _urlController =
TextEditingController(text: 'https://flutter.dev');
@override
void initState() {
super.initState();
_initRuntime();
}
Future<void> _initRuntime() async {
// 1. Create shared services context
final context = FixitRuntimeContext(
logger: FixitLogger(label: 'MyApp'),
events: FixitEventBus(),
cache: FixitCacheManager(),
cookies: FixitCookieManager(),
session: FixitSessionManager(),
);
// 2. Create runtime and controller
_runtime = FixitRuntime.create(1, context);
_controller = FixitWebViewController(_runtime);
// 3. Register a bridge handler BEFORE initialize
_controller.registerBridgeHandler('getUser', (message) async {
// This is called when JS posts a message with handlerName: 'getUser'
final userId = message.data['id'];
return {
'id': userId,
'name': 'Alice',
'email': 'alice@example.com',
};
});
// 4. Build config and initialize
final config = FixitRuntimeConfig.builder()
.setInitialUrl(_urlController.text)
.enableBridge()
.setJavaScriptEnabled(true)
.setDomStorageEnabled(true)
.build();
await _runtime.initialize(config);
if (mounted) setState(() => _ready = true);
}
@override
void dispose() {
_runtime.dispose();
_urlController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Fixit WebView')),
body: _ready ? _buildWebView() : const Center(child: CircularProgressIndicator()),
);
}
Widget _buildWebView() {
return Column(
children: [
// URL bar
Padding(
padding: const EdgeInsets.all(8.0),
child: Row(
children: [
Expanded(
child: TextField(
controller: _urlController,
decoration: const InputDecoration(
hintText: 'Enter URL',
border: OutlineInputBorder(),
contentPadding: EdgeInsets.symmetric(horizontal: 12),
isDense: true,
),
onSubmitted: (url) => _controller.loadUrl(url),
),
),
const SizedBox(width: 8),
ElevatedButton(
onPressed: () => _controller.loadUrl(_urlController.text),
child: const Text('Go'),
),
],
),
),
// Progress bar
ValueListenableBuilder<double>(
valueListenable: _controller.progress,
builder: (_, progress, __) =>
progress < 1.0 ? LinearProgressIndicator(value: progress) : const SizedBox.shrink(),
),
// WebView
Expanded(child: FixitWebView(controller: _controller)),
// Navigation controls
SafeArea(
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 4),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
ValueListenableBuilder<bool>(
valueListenable: _controller.canGoBack,
builder: (_, can, __) => IconButton(
icon: const Icon(Icons.arrow_back),
onPressed: can ? () => _controller.goBack() : null,
),
),
ValueListenableBuilder<bool>(
valueListenable: _controller.canGoForward,
builder: (_, can, __) => IconButton(
icon: const Icon(Icons.arrow_forward),
onPressed: can ? () => _controller.goForward() : null,
),
),
IconButton(
icon: const Icon(Icons.refresh),
onPressed: () => _controller.reload(),
),
],
),
),
),
],
);
}
}
2. Web Page with Bridge Integration #
Create an index.html that your WebView loads. This demonstrates how the JS side communicates with Dart:
<!DOCTYPE html>
<html>
<body>
<h1>Fixit Bridge Demo</h1>
<button onclick="getUser()">Get User Data</button>
<button onclick="sendToDart()">Send Custom Event</button>
<p id="output"></p>
<script>
// Send a message to Dart via the bridge
function sendToDart() {
window.FixitBridge.postMessage(JSON.stringify({
handlerName: 'getUser',
action: 'fetch',
data: { id: 42 },
callbackId: 'req_' + Date.now()
}));
}
// Receive messages from Dart
window.addEventListener('fixit-bridge', function(e) {
// e.detail is a JSON string from Dart
try {
const msg = JSON.parse(e.detail);
document.getElementById('output').textContent =
'From Dart: ' + JSON.stringify(msg.data);
} catch(err) {
console.error('Bridge parse error:', err);
}
});
// Also listen for RPC responses
getVersion();
function getVersion() {
window.FixitBridge.postMessage(JSON.stringify({
handlerName: 'getAppVersion',
action: 'version',
data: {},
callbackId: 'req_version'
}));
}
</script>
</body>
</html>
3. How It Works #
- Runtime Initialization —
FixitRuntime.create()creates a named WebView instance with shared services - Controller —
FixitWebViewControllerwraps the runtime and provides the full API surface - Bridge Registration —
registerBridgeHandler('getUser', ...)registers a Dart handler that JS can call - Configuration —
FixitRuntimeConfig.builder()builds the WebView settings - Widget —
FixitWebView(controller: _controller)renders the native WebView - JS Side — The web page uses
window.FixitBridge.postMessage()and listens forfixit-bridgeevents
Architecture #
┌─────────────────────────────────────────────────────┐
│ Flutter App │
│ ┌───────────────────────────────────────────────┐ │
│ │ FixitWebView (Widget) │ │
│ │ Splash, Loader, Crash UI, Pull-to-Refresh │ │
│ └───────────────────┬───────────────────────────┘ │
│ │ │
│ ┌───────────────────▼───────────────────────────┐ │
│ │ FixitWebViewController (API Surface) │ │
│ │ Navigation | Bridge | Upload | Download | ... │ │
│ └───────┬──────────────┬──────────────┬────────┘ │
│ │ │ │ │
│ ┌───────▼──────┐ ┌────▼─────┐ ┌──────▼───────┐ │
│ │ FixitRuntime │ │ JS Bridge│ │ Engines │ │
│ │ (Lifecycle) │ │ ↔ Web │ │ Nav,Upload, │ │
│ │ (Pool) │ │ Page │ │ Download,... │ │
│ └───────┬───────┘ └─────────┘ └──────────────┘ │
│ │ │
│ ┌───────▼───────────────────────────────────────┐ │
│ │ Platform Interface (Pigeon + Channels) │ │
│ └───────┬───────────────────────────────────────┘ │
│ │ │
├──────────┼──────────────────────────────────────────┤
│ ┌───────▼────────┐ ┌───────────────────┐ │
│ │ Android │ │ iOS │ │
│ │ (WebView) │ │ (WKWebView) │ │
│ └────────────────┘ └───────────────────┘ │
└─────────────────────────────────────────────────────┘
The SDK follows a bridge + module architecture:
- FixitRuntime owns the WebView lifecycle and exposes platform APIs
- FixitWebViewController provides the Dart-side API surface for all operations
- JavaScript Bridge enables bidirectional communication between the web page and native Dart modules
- Engines (navigation, offline, upload, download, theme) are pluggable modules accessed through the controller
Core Concepts #
FixitWebView Widget #
The main widget that renders the native WebView. It handles:
- White flash prevention (splash → fade transition via
AnimatedOpacity) - Custom page-load loader overlay
- Crash recovery overlay
- Pull-to-refresh
- Lifecycle management (pause/resume)
FixitWebView(
controller: _controller,
config: config, // optional, can also set via controller
onRefresh: () => _controller.reload(),
)
Config options that affect the widget appearance:
final config = FixitRuntimeConfig.builder()
.enableWhiteFlashPrevention(
splashBuilder: (context) => const Center(
child: CircularProgressIndicator(),
),
)
.setSplashTransitionDuration(const Duration(milliseconds: 500))
.enableCustomLoader(
loaderBuilder: (context, progress) => Container(
color: Colors.black26,
child: Center(
child: CircularProgressIndicator(value: progress),
),
),
)
.enableCrashRecovery(
crashOverlayBuilder: (context) => Container(
color: Colors.black87,
child: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.error, size: 48, color: Colors.red),
const SizedBox(height: 16),
ElevatedButton(
onPressed: () => _controller.reload(),
child: const Text('Reload'),
),
],
),
),
),
)
.enablePullToRefresh()
.build();
FixitWebViewController #
The primary API surface for controlling the WebView. All interactions go through this object.
Navigation:
| Method | Description |
|---|---|
loadUrl(url) |
Navigate to a URL |
loadHtmlString(html) |
Load raw HTML |
goBack() / goForward() |
Navigate history |
reload() |
Reload current page |
stopLoading() |
Stop page load |
reload() |
Reload current page |
Observables (ValueNotifier):
| Observable | Type | Description |
|---|---|---|
currentUrl |
String? |
Current page URL |
loading |
bool |
Whether page is loading |
progress |
double |
Load progress (0.0–1.0) |
pageTitle |
String? |
Page title from <title> |
canGoBack |
bool |
Whether back navigation is available |
canGoForward |
bool |
Whether forward navigation is available |
Streams (for events you listen to):
| Stream | Data Type | Description |
|---|---|---|
diagnosticsStream |
DiagnosticsEvent |
Startup timeline milestones |
onCrash |
WebViewCrashEvent |
Renderer crash events |
onRestart |
void |
Automatic WebView restorations |
Methods for features:
| Method | Description |
|---|---|
registerBridgeHandler(name, handler) |
Register a JS bridge handler |
postBridgeMessage(message) |
Send a message from Dart to JS |
invoke(action, data) |
RPC-style Dart→JS call that awaits response |
evaluateJavascript(js) |
Execute JS (fire-and-forget) |
runJavascriptReturningResult(js) |
Execute JS and get a result |
setCookie(url, name, value) |
Set a browser cookie |
getCookies(url) |
Get cookies as strings |
clearCookies() |
Clear all cookies |
checkPermission(type) |
Check permission status |
requestPermission(type) |
Request a runtime permission |
pause() / resume() |
Manual lifecycle control |
dispose() |
Release all resources |
FixitRuntime #
The central runtime that owns a WebView instance. Created via:
final runtime = FixitRuntime.create(viewId: 1, context: runtimeContext);
View ID must be unique per WebView instance. Use 1 for a single WebView, increment for multiple instances.
FixitRuntimeContext provides shared services:
logger—FixitLoggerfor debuggingevents—FixitEventBusfor event propagationcache—FixitCacheManagerfor disk cachecookies—FixitCookieManagerfor persistent cookiessession—FixitSessionManagerfor key-value session data
FixitRuntimeConfig #
Configuration object created via the builder pattern. Every option has a sensible default.
| Builder Method | Default | Description |
|---|---|---|
setInitialUrl(url) |
— | First URL to load |
enableBridge() |
off | Enable JS ↔ Dart bridge |
enablePooling() |
off | Enable WebView instance pooling |
enableOffline() |
off | Enable offline caching |
enableDownloads() |
off | Enable file downloads |
setJavaScriptEnabled(bool) |
true |
Enable JavaScript |
setDomStorageEnabled(bool) |
true |
Enable DOM storage |
setCacheEnabled(bool) |
true |
Enable WebView cache |
setAllowFileAccess(bool) |
false |
Allow file:// URLs |
setAcceptThirdPartyCookies(bool) |
false |
Accept 3rd party cookies |
setMediaPlaybackRequiresGesture(bool) |
false |
Require user gesture for media |
setMaxUploadFileSize(int) |
no limit | Max upload file size |
setAllowedUploadMimeTypes(List) |
all | Allowed upload MIME types |
addNavigationWhitelist(List) |
none | Whitelisted URL patterns |
addNavigationBlacklist(List) |
none | Blacklisted URL patterns |
setExternalSchemes(List) |
none | External URL schemes |
enableDiagnostics(level) |
off | Performance diagnostics level |
setUserAgent(string) |
default | Custom user agent |
setDownloadDirectory(path) |
default | Download directory |
setAutoOpenDownload(bool) |
false |
Auto-open downloaded files |
setThemeConfig(config) |
none | Theme engine configuration |
enableWhiteFlashPrevention() |
off | Show splash while loading |
setSplashTransitionDuration(Duration) |
300ms | Splash → WebView fade duration |
enableCustomLoader() |
off | Show loader during navigation |
enableCrashRecovery() |
off | Show crash overlay |
enablePullToRefresh() |
off | Enable pull-to-refresh |
Lifecycle #
┌────────────┐
│ Created │ FixitRuntime.create()
└─────┬──────┘
│
┌─────▼──────┐
│ Initialized│ runtime.initialize(config)
| (loading) │
└─────┬──────┘
│
┌─────▼──────┐
│ Active │ WebView is visible and interactive
│ (resumed) │
└─────┬──────┘
│
┌─────▼──────┐
│ Paused │ App backgrounded: JS timers suspended,
│ │ cookies flushed to disk
└─────┬──────┘
│
┌─────▼──────┐
│ Active │ App foregrounded: JS timers resumed,
│ (resumed) │ focus restored
└─────┬──────┘
│
┌─────▼──────┐
│ Disposed │ runtime.dispose(): all resources released
└────────────┘
Call runtime.dispose() in your widget's dispose() method when the WebView is no longer needed.
JavaScript Bridge #
The bridge is the core differentiator of Fixit WebView. It enables seamless bidirectional communication between Dart and JavaScript running inside the WebView.
How It Works #
Dart ──── evaluateJavascript ────► window.dispatchEvent(CustomEvent)
│ │
│ ▼
│ JS listens for 'fixit-bridge'
│ │
│ ▼
│ JS processes the message
│ │
│ ▼
◄────────── FixitBridge.postMessage ──┘
Dart → JS:
Dart sends messages by injecting JavaScript that dispatches a CustomEvent('fixit-bridge', { detail: ... }) on window. Your page JS listens for this event.
JS → Dart:
JavaScript calls window.FixitBridge.postMessage(jsonString). The native WebView layer intercepts this call and forwards the deserialized message to the registered Dart handler.
JS Side Setup #
Your web page needs two things to communicate with Dart:
- Listen for incoming messages from Dart via
fixit-bridgeevent - Send outgoing messages to Dart via
window.FixitBridge.postMessage()
// ====== bridge.js — Include this in your web page ======
(function() {
'use strict';
// --- Send messages to Dart ---
window.FixitBridge = window.FixitBridge || {
postMessage: function(jsonString) {
// Native layer intercepts this call.
// On Android: @JavascriptInterface annotated method
// On iOS: WKUserContentController message handler
// The jsonString must be a valid JSON string.
}
};
// --- Receive messages from Dart ---
window.addEventListener('fixit-bridge', function(event) {
// event.detail is a JSON string from Dart
const message = JSON.parse(event.detail);
switch (message.action) {
case 'navigate':
window.location.href = message.data.url;
break;
case 'showToast':
alert(message.data.text);
break;
case 'updateUser':
updateUserUI(message.data);
break;
default:
console.log('Unknown action:', message.action);
}
});
// --- Helper: Send a structured message to Dart ---
window.sendToDart = function(handlerName, action, data, callbackId) {
window.FixitBridge.postMessage(JSON.stringify({
handlerName: handlerName,
action: action,
data: data || {},
callbackId: callbackId || ('cb_' + Date.now())
}));
};
// --- Helper: Update UI with user data ---
function updateUserUI(user) {
var el = document.getElementById('user-info');
if (el) {
el.textContent = user.name + ' (' + user.email + ')';
}
}
})();
Dart Side Setup #
On the Dart side, register handlers for messages coming from JavaScript:
// Register a handler before calling runtime.initialize()
controller.registerBridgeHandler('getUser', (message) async {
// message.data contains the JS payload
final id = message.data['id'] as int?;
// Fetch from your backend or local storage
final user = await _fetchUserFromApi(id);
// Return data to JS (it becomes the RPC response)
return user;
});
// Register another handler
controller.registerBridgeHandler('getAppVersion', (message) async {
return {
'version': '1.0.0',
'buildNumber': 42,
'platform': Platform.isAndroid ? 'Android' : 'iOS'
};
});
// Send a message TO the page (fire-and-forget)
await controller.postBridgeMessage(
BridgeMessage(
handlerName: 'system',
action: 'showToast',
data: {'text': 'Hello from Dart!'},
callbackId: 'msg_1',
),
);
// RPC-style: invoke an action on JS and wait for response
final result = await controller.invoke('getUser', {'id': 42});
print(result); // {'id': 42, 'name': 'Alice', ...}
BridgeMessage Structure #
| Field | Type | Description |
|---|---|---|
handlerName |
String |
Name of the handler to invoke on the receiving side |
action |
String |
Action to perform (e.g. 'fetch', 'update', 'delete') |
data |
Map<String, dynamic> |
The payload data |
callbackId |
String |
Correlation ID used to match responses to requests |
Bridge Security #
final validator = BridgeValidator(
config: BridgeSecurityConfig(
allowedOrigins: ['https://example.com', 'https://myapp.com'],
maxMessageSize: 1024 * 512, // 512 KB max payload
timeout: const Duration(seconds: 30),
),
);
if (validator.isValidOrigin('https://example.com')) {
// Process the bridge message
}
if (validator.isValidMessageSize(jsonString.length)) {
// Message is within size limit
}
Real-World: Login Flow #
A common use case is having the web page handle login and notify Dart of the result:
Dart side:
// Register login handler
controller.registerBridgeHandler('auth', (message) async {
switch (message.action) {
case 'loginSuccess':
final token = message.data['accessToken'] as String;
final user = message.data['user'] as Map<String, dynamic>;
await _sessionManager.setValue('auth_token', token);
await _sessionManager.setValue('user', user);
return {'status': 'ok'};
case 'logout':
await _sessionManager.clearSession();
return {'status': 'logged_out'};
case 'checkSession':
final token = _sessionManager.getValue('auth_token');
return {'hasSession': token != null, 'token': token};
}
return {'error': 'unknown_action'};
});
// Send auth state to page after bridge is ready
await controller.postBridgeMessage(
BridgeMessage(
handlerName: 'auth',
action: 'sessionRestored',
data: {
'token': _sessionManager.getValue('auth_token'),
'user': _sessionManager.getValue('user'),
},
callbackId: 'session_sync',
),
);
JS side:
// After successful login on the page
function onLoginSuccess(user, token) {
window.sendToDart('auth', 'loginSuccess', {
accessToken: token,
user: user
});
}
// On page load, check if already logged in
window.addEventListener('fixit-bridge', function(e) {
const msg = JSON.parse(e.detail);
if (msg.handlerName === 'auth' && msg.action === 'sessionRestored') {
if (msg.data.token) {
// Session restored — skip login screen
restoreSession(msg.data.token, msg.data.user);
}
}
});
Navigation & Security #
Navigation Engine #
The FixitNavigationEngine provides URL interception, deep links, SSL error handling, and HTTP auth.
final engine = controller.navigationEngine;
// Listen to navigation requests
engine.onNavigationRequested.listen((request) {
print('Navigating to: ${request.url}');
print('Is main frame: ${request.isMainFrame}');
});
// Register URL routing rules
controller.registerNavigationRule(r'\.pdf$', 'external');
controller.registerNavigationRule(r'^https://api\.', 'block');
// Register external URL schemes
controller.registerExternalSchemes(['tel', 'mailto', 'sms', 'facetime']);
// Handle deep links
controller.setDeepLinkHandler((url) {
if (url.startsWith('myapp://')) {
// Parse and handle the deep link
final route = url.replaceFirst('myapp://', '');
print('Deep link received: $route');
return true; // Consume the navigation (WebView won't load it)
}
return false; // Let WebView handle it normally
});
SSL Errors #
engine.onSslError.listen((error) async {
print('SSL error on ${error.url}: ${error.error}');
print('Error code: ${error.code}');
// Accept the error and proceed
await controller.acceptSslError(error.url);
// Or deny it (page will not load)
// await controller.denySslError(error.url);
});
HTTP Authentication #
engine.onHttpAuthRequest.listen((request) async {
print('Auth required: ${request.host}:${request.port}');
print('Realm: ${request.realm}');
// Provide credentials
await controller.httpAuthResponse(
requestId: request.requestId,
username: 'user',
password: 'pass',
);
// Or cancel authentication:
// await controller.cancelHttpAuthResponse(request.requestId);
});
Blocked Navigation #
engine.onNavigationBlocked.listen((event) {
print('Blocked: ${event.url}');
print('Reason: ${event.reason}');
// Possible reasons: whitelist, blacklist, rule, deepLink
});
Navigation History #
final history = engine.history;
print('Back: ${history.backStack.length} entries');
print('Forward: ${history.forwardStack.length} entries');
print('Current URL: ${history.current?.url}');
// Navigate
await controller.goBack();
await controller.goForward();
Security Config #
await controller.updateSecurityConfig(
mixedContentMode: 1, // 0=never, 1=compat, 2=always
safeBrowsingEnabled: true, // Android only
zoomEnabled: true,
);
OAuth Interceptor #
Automatically detects OAuth callback URLs during navigation:
// Register a callback pattern
controller.registerOAuthCallbackPattern('callback');
// Check if a URL is an OAuth callback
final detected = controller.checkOAuthCallback(
'https://myapp.com/callback?code=auth_code_xyz&state=abc123',
);
print('Is OAuth callback: ${detected.isCallback}');
print('Code: ${detected.code}');
print('State: ${detected.state}');
// Listen for detected OAuth callbacks
controller.oAuthInterceptor.onOAuthCallback.listen((callback) {
print('OAuth code: ${callback.code}');
print('Access token: ${callback.accessToken}');
print('State: ${callback.state}');
});
Upload Engine #
Handles file upload requests triggered by <input type="file"> elements in the web page.
// Listen for upload requests
controller.uploadEngine.onUploadRequested.listen((request) {
print('Upload requested: #${request.requestId}');
print('Accept types: ${request.acceptTypes}');
print('Capture enabled: ${request.isCaptureEnabled}');
print('Multiple: ${request.allowsMultipleSelection}');
// Provide file paths in response
await controller.selectFiles(request.requestId, [
'/storage/emulated/0/DCIM/photo.jpg',
]);
});
// Track upload progress
controller.uploadEngine.onUploadProgress.listen((progress) {
print('${progress.bytesSent}/${progress.totalBytes} '
'(${(progress.fraction * 100).toStringAsFixed(1)}%)');
});
// Cancel an upload
await controller.cancelUpload(requestId: 1);
UploadRequest fields:
| Field | Type | Description |
|---|---|---|
requestId |
int |
Unique request identifier |
acceptTypes |
List<String> |
Accepted MIME types (e.g. ['image/*']) |
isCaptureEnabled |
bool |
Whether camera/gallery capture is available |
allowsMultipleSelection |
bool |
Whether multiple files can be selected |
Download Engine #
Manages file downloads triggered by the web page or initiated programmatically.
// Listen for download requests
controller.downloadEngine.onDownloadRequested.listen((request) {
print('Download: ${request.url}');
print('MIME: ${request.mimeType}');
print('Size: ${request.contentLength} bytes');
// Accept and start the download
await controller.acceptDownload(request.requestId);
// Or reject:
// await controller.cancelDownload(requestId: request.requestId);
});
// Track download progress
controller.downloadEngine.onDownloadProgress.listen((progress) {
print('${progress.receivedBytes}/${progress.totalBytes} '
'(${(progress.fraction * 100).toStringAsFixed(1)}%)');
});
// Listen for completion
controller.downloadEngine.onDownloadCompleted.listen((result) {
print('Download #${result.requestId} saved to: ${result.filePath}');
});
// Listen for failures
controller.downloadEngine.onDownloadFailed.listen((failure) {
final (requestId, error) = failure;
print('Download #$requestId failed: $error');
});
// Open a downloaded file with the system handler
await controller.openDownloadFile('/path/to/file.pdf', 'application/pdf');
DownloadRequest fields:
| Field | Type | Description |
|---|---|---|
requestId |
int |
Unique request identifier |
url |
String |
Download URL |
mimeType |
String |
MIME type of the file |
contentLength |
int |
Expected file size (0 if unknown) |
contentDisposition |
String |
Content-Disposition header value |
Offline Engine #
Provides caching, connectivity monitoring, and fallback for offline scenarios.
final offline = controller.offlineEngine;
// Check connectivity
offline.onConnectivityChanged.listen((state) {
print(state == ConnectivityState.online ? 'Online' : 'Offline');
if (state == ConnectivityState.offline) {
// Show offline indicator
}
});
// Set cache strategy
offline.strategy = CacheStrategy.cacheFirst;
// Options: cacheFirst, networkFirst, networkOnly, cacheOnly
// Cache a response manually
await offline.cacheResponse(
'https://example.com/page',
'<html><body><h1>Cached</h1></body></html>',
'text/html',
);
// Retrieve cached response
final cached = await offline.getCached('https://example.com/page');
if (cached != null) {
print('Serving cached: ${cached.data}');
}
// Pre-cache critical resources
await offline.preCache([
'https://example.com/style.css',
'https://example.com/app.js',
'https://example.com/logo.png',
]);
// Get all cached URLs
final urls = await offline.getCachedUrls();
// Clear cache
await offline.clearCache();
// Retry queue for failed requests
offline.enqueueRetry('https://example.com/failed-resource');
print('Pending retries: ${offline.pendingRetries}');
// Custom offline fallback HTML
offline.offlineFallbackHtml = '''
<html>
<body style="text-align:center;padding:40px;">
<h1>You're Offline</h1>
<p>Please check your connection.</p>
</body>
</html>
''';
CacheStrategy options:
| Strategy | Behavior |
|---|---|
cacheFirst |
Serve from cache if available; fetch from network otherwise |
networkFirst |
Fetch from network first; fall back to cache on failure |
networkOnly |
Always fetch from network |
cacheOnly |
Serve only from cache |
Theme Engine #
Injects CSS themes into the WebView page, reacting to system brightness changes.
final theme = controller.themeEngine;
// Inject a custom theme
await theme.injectTheme(
ThemeDefinition(
name: 'dark_mode',
css: '''
body { background: #121212 !important; color: #e0e0e0 !important; }
a { color: #bb86fc !important; }
.header { background: #1e1e1e !important; }
''',
),
);
// Reset to no custom theme
await theme.resetTheme();
// Auto-detect system brightness (default: true)
theme.autoDetect = true;
// Or use FixitThemeConfig for color-based generation
final config = FixitThemeConfigBuilder()
.setBackgroundColor(const Color(0xFF121212))
.setTextColor(const Color(0xFFE0E0E0))
.setAccentColor(const Color(0xFFBB86FC))
.setSurfaceColor(const Color(0xFF1E1E1E))
.setPreferredBrightness(Brightness.dark)
.build();
await theme.applyConfig(config);
Permissions #
Check and request runtime permissions for camera, microphone, and location.
// Check current permission state
final cameraStatus = await controller.checkPermission(PermissionType.camera);
final micStatus = await controller.checkPermission(PermissionType.microphone);
final locationStatus = await controller.checkPermission(PermissionType.location);
// Request permission (prompts user if needed)
final result = await controller.requestPermission(PermissionType.camera);
if (result == PermissionStatus.granted) {
print('Camera access granted');
} else if (result == PermissionStatus.deniedForever) {
// Show a dialog directing user to Settings
print('Camera permission denied permanently');
}
// Track which features have been used
controller.featureRegistry.isEnabled(NativeFeatureType.camera);
PermissionStatus values:
| Status | Description |
|---|---|
granted |
Permission granted |
denied |
Permission denied (can be re-requested) |
deniedForever |
Permission denied permanently (go to Settings) |
restricted |
Permission restricted by parental controls |
limited |
Granted with limited access (iOS photo library) |
Cookies & Sessions #
Cookie Manager #
Persistent cookie storage with disk backup and platform-level synchronization.
// Set a cookie (domain-scoped)
await controller.setCookie('https://example.com', 'session_id', 'abc123');
await controller.setCookie('https://example.com', 'theme', 'dark');
// Get cookies for a URL
final cookies = await controller.getCookies('https://example.com');
// Returns: ['session_id=abc123', 'theme=dark']
// Get cookies as a map
final cookieMap =
await controller.cookieManager.getCookiesAsMap('https://example.com');
print(cookieMap['session_id']); // 'abc123'
print(cookieMap['theme']); // 'dark'
// Clear cookies for a specific URL
await controller.cookieManager.clearCookiesForUrl('https://example.com');
// Clear all cookies
await controller.clearCookies();
Session Manager #
Key-value session data with automatic persistence to disk (survives app restarts).
final session = controller.sessionManager;
// Store session data
await session.setValue('user_id', 42);
await session.setValue('auth_token', 'tok_abc123');
await session.setValue('onboarding_complete', true);
// Retrieve session data
final userId = session.getValue('user_id'); // 42
final hasToken = session.has('auth_token'); // true
final hasSession = session.hasSession; // true
// Get all data as a map
final allData = session.snapshot();
print(allData); // {'user_id': 42, 'auth_token': 'tok_abc123', ...}
// Remove a specific key
await session.removeValue('auth_token');
// Clear all session data
await controller.clearSession();
Performance Diagnostics #
Capture startup timeline and performance metrics.
// Enable diagnostics in config
final config = FixitRuntimeConfig.builder()
.enableDiagnostics(level: DiagnosticsLevel.startup)
.build();
// Listen for startup timeline events
controller.diagnosticsStream.listen((event) {
for (final milestone in event.milestones) {
print('${milestone.name}: ${milestone.epochMs}ms');
}
});
// Or use the performance engine directly
final perf = FixitPerformanceEngine(runtimeInfo: runtime.info);
perf.recordLoadTime(const Duration(milliseconds: 1500));
perf.recordMetrics(FixitPerformanceMetrics(
fps: 60.0,
memoryBytes: 52428800, // 50 MB
cpuPercent: 15.0,
loadTime: Duration(seconds: 2),
));
// Export performance report
final report = perf.exportReport();
print(report);
Diagnostics milestones (T0–T5):
| Milestone | Description |
|---|---|
T0 |
Runtime created |
T1 |
Config initialized |
T2 |
Native WebView created |
T3 |
URL loading started |
T4 |
Page started loading |
T5 |
Page finished loading |
Force Update #
Check for app updates from a remote JSON endpoint and show a blocking or dismissible update screen.
// Check for updates
final result = await FixitForceUpdate.check(
currentVersion: '1.0.0',
minVersionUrl: 'https://example.com/version.json',
);
if (result.updateRequired) {
// Show blocking update screen (user MUST update)
if (result.forceUpdate) {
await FixitForceUpdate.showForceUpdateScreen(
context: context,
result: result,
barrierDismissible: false, // Blocking — user cannot dismiss
onUpdate: () {
// Open Play Store / App Store
launchUrl(Uri.parse(result.updateUrl ?? ''));
},
);
} else {
// Show dismissible update prompt
await FixitForceUpdate.showForceUpdateScreen(
context: context,
result: result,
barrierDismissible: true, // User can dismiss
);
}
} else {
print('App is up to date');
}
Remote JSON endpoint should return:
{
"minVersion": "1.0.2",
"force": true,
"latestVersion": "1.2.0",
"updateUrl": "https://play.google.com/store/apps/details?id=com.example.app"
}
Fields:
| Field | Type | Description |
|---|---|---|
minVersion |
String |
Minimum required version (semver) |
force |
Boolean |
Whether update is mandatory |
latestVersion |
String |
Latest available version |
updateUrl |
String |
URL to download the update |
Crash Recovery & Splash #
Crash Recovery #
The WebView automatically detects renderer crashes and can display a recovery overlay.
final config = FixitRuntimeConfig.builder()
.enableCrashRecovery(
crashOverlayBuilder: (context) => Container(
color: Colors.black87,
child: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.error_outline, size: 64, color: Colors.red),
const SizedBox(height: 16),
const Text(
'Something went wrong',
style: TextStyle(color: Colors.white, fontSize: 18),
),
const SizedBox(height: 24),
ElevatedButton.icon(
onPressed: () => controller.reload(),
icon: const Icon(Icons.refresh),
label: const Text('Reload'),
),
],
),
),
),
)
.build();
// Listen for crash events
controller.onCrash.listen((event) {
print('WebView crashed (viewId: ${event.viewId})');
// You could send a crash report to your analytics
});
// Listen for automatic restarts
controller.onRestart.listen((_) {
print('WebView restored after crash');
});
White Flash Prevention / Splash Transition #
Prevents the white flash that occurs when a WebView loads by showing a splash screen that fades into the WebView.
final config = FixitRuntimeConfig.builder()
.enableWhiteFlashPrevention(
splashBuilder: (context) => Container(
color: Colors.white,
child: const Center(
child: CircularProgressIndicator(),
),
),
)
.setSplashTransitionDuration(const Duration(milliseconds: 500))
.build();
Custom Page-Load Loader #
Show a progress overlay during page navigation:
final config = FixitRuntimeConfig.builder()
.enableCustomLoader(
loaderBuilder: (context, progress) => Container(
color: Colors.black26,
child: Center(
child: SizedBox(
width: 120,
child: LinearProgressIndicator(value: progress),
),
),
),
)
.build();
Putting It All Together #
A complete Flutter app that uses multiple features together: bridge, navigation, offline, theme, and force update.
import 'package:flutter/material.dart';
import 'package:fixit_core/fixit_core.dart';
import 'package:fixit_webview/fixit_webview.dart';
void main() => runApp(const MyApp());
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
title: 'Fixit Production App',
theme: ThemeData(colorSchemeSeed: Colors.indigo),
home: const HomeScreen(),
);
}
}
class HomeScreen extends StatefulWidget {
const HomeScreen({super.key});
@override
State<HomeScreen> createState() => _HomeScreenState();
}
class _HomeScreenState extends State<HomeScreen> {
late final FixitRuntime _runtime;
late final FixitWebViewController _controller;
bool _ready = false;
@override
void initState() {
super.initState();
_init();
}
Future<void> _init() async {
// 1. Shared services
final context = FixitRuntimeContext(
logger: FixitLogger(label: 'ProductionApp'),
events: FixitEventBus(),
cache: FixitCacheManager(),
cookies: FixitCookieManager(),
session: FixitSessionManager(),
);
// 2. Runtime & controller
_runtime = FixitRuntime.create(1, context);
_controller = FixitWebViewController(_runtime);
// 3. Check for force update
final forceResult = await FixitForceUpdate.check(
currentVersion: '1.0.0',
minVersionUrl: 'https://example.com/version.json',
);
if (forceResult.updateRequired && mounted) {
await FixitForceUpdate.showForceUpdateScreen(
context: context,
result: forceResult,
barrierDismissible: !forceResult.forceUpdate,
onUpdate: () {
// Open app store
},
);
}
// 4. Register bridge handlers
_controller.registerBridgeHandler('getUser', (message) async {
return {'id': 42, 'name': 'Alice'};
});
_controller.registerBridgeHandler('auth', (message) async {
if (message.action == 'loginSuccess') {
await _runtime.context.session
.setValue('auth_token', message.data['token']);
return {'status': 'ok'};
}
return {'error': 'unknown'};
});
// 5. Configure and initialize
final config = FixitRuntimeConfig.builder()
.setInitialUrl('https://example.com')
.enableBridge()
.enableOffline()
.enableDownloads()
.enableWhiteFlashPrevention()
.enableCrashRecovery()
.enablePullToRefresh()
.setJavaScriptEnabled(true)
.setDomStorageEnabled(true)
.setCacheEnabled(true)
.addNavigationWhitelist(['*.example.com'])
.build();
await _runtime.initialize(config);
if (mounted) setState(() => _ready = true);
}
@override
void dispose() {
_runtime.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('My App'),
actions: [
IconButton(
icon: const Icon(Icons.refresh),
onPressed: () => _controller.reload(),
),
],
),
body: _ready
? FixitWebView(
controller: _controller,
onRefresh: () => _controller.reload(),
)
: const Center(child: CircularProgressIndicator()),
);
}
}
Troubleshooting & FAQ #
Bridge doesn't work #
Problem: window.FixitBridge is undefined in the web page.
Solution 1: Make sure enableBridge() is called in the config:
final config = FixitRuntimeConfig.builder()
.enableBridge() // ← Required!
.build();
Solution 2: Check that the page is fully loaded before calling bridge methods. Use the loading notifier:
_valueListenableBuilder<bool>(
valueListenable: _controller.loading,
builder: (_, isLoading, __) {
if (!isLoading) {
// Page loaded — safe to use bridge
}
return ...;
},
);
WebView shows white screen #
Problem: The WebView displays a white screen instead of the page.
Solution 1: Enable white flash prevention with a splash:
.enableWhiteFlashPrevention(
splashBuilder: (context) => Container(
color: Colors.white,
child: const Center(child: CircularProgressIndicator()),
),
)
Solution 2: Check that the URL is accessible and HTTPS. Some sites block WebView user agents.
Navigation rules not blocking ads #
Problem: Blacklisted URLs still load.
Solution: Use glob-style patterns with wildcards:
.addNavigationBlacklist(['*://*.doubleclick.net/*', '*://*.googlesyndication.com/*'])
.addNavigationBlacklist(['*://*.ads.*'])
Downloads not working #
Problem: Download requests are not intercepted.
Solution 1: Make sure enableDownloads() is in the config.
Solution 2: Verify the MIME type is in the allowed list. Add explicit types:
.setAllowedUploadMimeTypes(['image/*', 'application/pdf', 'video/*'])
Offline engine not caching #
Problem: Resources are not served from cache when offline.
Solution 1: Ensure enableOffline() is in the config.
Solution 2: Set the strategy to cacheFirst or networkFirst:
offline.strategy = CacheStrategy.cacheFirst;
Solution 3: Pre-cache critical resources explicitly:
await offline.preCache(['https://example.com/', 'https://example.com/app.js']);
MissingPluginException #
Problem: MissingPluginException at runtime.
Solution: This usually means the plugin wasn't registered. Clean and rebuild:
flutter clean
flutter pub get
flutter run
SecurityException crash on Android #
Problem: Crash with SecurityException on some Android devices.
Solution: Add the network state permission to AndroidManifest.xml:
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
Best Practices #
-
Register bridge handlers before
runtime.initialize()— Handlers are wired up during initialization. Registering after may miss early messages. -
Dispose the runtime in your widget's
dispose()— Always call_runtime.dispose()to prevent memory leaks:@override void dispose() { _runtime.dispose(); super.dispose(); } -
Use
mountedcheck beforesetState— After async operations, check if the widget is still mounted:await _runtime.initialize(config); if (mounted) setState(() => _ready = true); -
Set initial URL on the first build — Pass the initial URL via config, not by calling
loadUrl()ininitState. -
Use
ValueListenableBuilderfor reactive UI — Instead of polling, use the built-inValueListenableproperties like_controller.progress,_controller.loading,_controller.canGoBack. -
Configure navigation rules early — Set whitelist/blacklist and external schemes during config setup, not after the page loads.
-
Pre-cache for offline-first apps — If your app needs to work offline, call
offline.preCache()during initialization. -
Handle OAuth callbacks — Register callback patterns before navigation starts to capture redirects:
controller.registerOAuthCallbackPattern('callback');
API Reference #
Widget #
| Class | Description |
|---|---|
FixitWebView |
The main WebView widget. Handles splash, crash, loader, pull-to-refresh. |
Controller #
| Class | Description |
|---|---|
FixitWebViewController |
Primary controller for all WebView operations. |
ScrollUpdate |
Holds current scroll position and delta. |
WebViewCrashEvent |
Describes a WebView renderer crash event. |
MemoryPressureLevel |
OS memory pressure levels: none, moderate, critical. |
Config #
| Class | Description |
|---|---|
FixitRuntimeConfig |
Configuration object with builder pattern. |
FixitRuntimeConfigBuilder |
Fluent builder for FixitRuntimeConfig. |
FixitCapabilities |
Tracks which capabilities are active. |
FixitCapability |
Enum: pooling, bridge, downloads, offline, performance, experimental, enterprise, crashRecovery, memoryPressure. |
FixitThemeConfig |
Holds color/brightness settings for CSS theme generation. |
FixitThemeConfigBuilder |
Fluent builder for FixitThemeConfig. |
FixitAIAdapter |
Abstract adapter for AI-powered page analysis. |
NativeFeatureRegistry |
Tracks which native device features have been used. |
NativeFeatureType |
Enum: camera, gallery, location, microphone. |
Bridge #
| Class | Description |
|---|---|
FixitBridgeManager |
Manages registration and dispatching of bridge messages. |
BridgeHandler |
Abstract handler for JS → Dart bridge messages. |
BridgeMessage |
Represents a message from JS via the bridge. |
BridgeRegistry |
Maps handler names to BridgeHandler instances. |
BridgeValidator |
Validates bridge messages against security constraints. |
BridgeSecurityConfig |
Security config: allowed origins, max size, timeout. |
Runtime #
| Class | Description |
|---|---|
FixitRuntime |
Central runtime owning a WebView instance. |
FixitRuntimeContext |
Shared services: logger, cache, cookies, session, events. |
FixitRuntimeInfo |
Metadata about a runtime instance. |
FixitExtensionPlugin |
Abstract extension point for FixitRuntime. |
Navigation #
| Class | Description |
|---|---|
FixitNavigationEngine |
Navigation interception, SSL errors, HTTP auth, deep links. |
NavigationRequest |
Represents a WebView navigation attempt. |
SslErrorEvent |
SSL error encountered by the WebView. |
HttpAuthRequest |
HTTP authentication challenge. |
BlockedNavigation |
A navigation blocked by the rules engine. |
NavigationHistory |
Tracks back/forward navigation history. |
NavigationEntry |
A single entry in the navigation history. |
FixitUrlRulesEngine |
Regex-based URL routing rules. |
UrlRule |
A rule that matches URLs against a regex pattern. |
FixitOAuthInterceptor |
Detects OAuth callback URLs during navigation. |
OAuthCallback |
Parsed result of an OAuth callback URL. |
FixitRequest |
HTTP request that can be intercepted. |
FixitRequestInterceptor |
Abstract request interceptor. |
FixitResponse |
HTTP response that can be intercepted. |
FixitResponseInterceptor |
Abstract response interceptor. |
DeepLinkHandler |
Typedef: bool Function(String url). |
Upload #
| Class | Description |
|---|---|
FixitUploadEngine |
Coordinates file uploads between WebView and native. |
UploadRequest |
A file upload request from the web page. |
UploadProgress |
Progress of an active upload. |
UploadResult |
Result of a completed upload. |
Download #
| Class | Description |
|---|---|
FixitDownloadEngine |
Coordinates file downloads between WebView and native. |
DownloadRequest |
A download request from the web page. |
DownloadProgress |
Progress of an active download. |
DownloadResult |
Result of a completed download. |
Offline #
| Class | Description |
|---|---|
FixitOfflineEngine |
Offline caching, connectivity, retry queue. |
CachedResponse |
A previously cached HTTP response. |
ConnectivityState |
Enum: online, offline. |
CacheStrategy |
Enum: cacheFirst, networkFirst, networkOnly, cacheOnly. |
Theme #
| Class | Description |
|---|---|
FixitThemeEngine |
Injects CSS themes into the WebView page. |
ThemeDefinition |
A named theme with CSS string. |
Permissions #
| Class | Description |
|---|---|
FixitPermissionManager |
Singleton for runtime permission checks/requests. |
PermissionType |
Enum: camera, microphone, location. |
PermissionStatus |
Enum: granted, denied, deniedForever, restricted, limited. |
Performance #
| Class | Description |
|---|---|
FixitPerformanceEngine |
Records FPS, memory, CPU, load time metrics. |
FixitPerformanceMetrics |
Snapshot of performance metrics. |
Force Update #
| Class | Description |
|---|---|
FixitForceUpdate |
Remote version check with static methods. |
FixitForceUpdateScreen |
Update dialog widget (blocking or dismissible). |
ForceUpdateResult |
Result of a version check. |
Cookies & Session #
| Class | Description |
|---|---|
FixitCookieManager |
Persistent cookie store with disk backup. |
FixitSessionManager |
Key-value session data with persistence. |
FixitCacheManager |
WebView cache lifecycle management. |
Platform Support #
| Platform | Minimum Version | Status |
|---|---|---|
| Android | 8 (API 26) | ✅ Production |
| iOS | 15.0 | ✅ Production |
Examples #
The example/ directory contains complete, runnable apps:
| Example | Location | Description |
|---|---|---|
| Basic | example/lib/main.dart |
Minimal WebView with URL bar |
| Advanced | example/lib/advanced/main.dart |
URL bar, JS console, navigation controls, progress tracking |
| Benchmark | example/lib/benchmark/main.dart |
Startup timeline diagnostics with JSON export |
License #
MIT — see LICENSE.