Flutter WebView Integration Guide
Overview
This Flutter WebView integration provides a solution for the microphone access challenge faced by Revrag's embed agent when implemented in mobile apps using WebView. The embed agent requires microphone access to enable voice-based AI assistance for website navigation, but traditional WebView implementations often fail to properly delegate microphone permissions to the embedded website.
Problem Statement
Revrag's embed agent is a JavaScript SDK that can be integrated into any website to provide AI-powered voice assistance. However, when mobile apps load websites containing the embed agent in a WebView, the microphone access required by the agent is often blocked or not properly delegated, preventing users from interacting with the AI assistant.
Prerequisites
- Flutter 3.0+
- iOS 13+ / Android API 21+
- Microphone permissions on target device
This integration requires proper setup of WebView microphone permissions and real-time audio communication capabilities. Check out the detailed documentation for flutter_inappwebview.
Installation
Add the required packages to your pubspec.yaml:
dependencies:
flutter:
sdk: flutter
flutter_inappwebview: ^6.0.0
permission_handler: ^11.3.1Then run:
flutter pub getSolution Overview
This project implements a Flutter WebView solution using the flutter_inappwebview package with proper microphone permission handling.
The solution:
- Requests native microphone permissions using the
permission_handlerpackage - Delegates permissions to the WebView when the embed agent requests microphone access
- Handles permission states across both native and WebView contexts
- Provides a seamless user experience for voice interactions
Key Features
- ✅ Native Microphone Permission Handling: Properly requests and manages microphone permissions at the app level
- ✅ WebView Permission Delegation: Seamlessly delegates permissions to the embedded website
- ✅ Cross-Platform Support: Works on both Android and iOS
- ✅ Error Handling: Comprehensive error handling and user feedback
- ✅ Configurable: Easy configuration for different websites and use cases
Android Configuration
1. Android Manifest Permissions
Add the following permissions to your android/app/src/main/AndroidManifest.xml:
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- Required permissions for WebView microphone access -->
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<application
android:label="@string/app_name"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher"
android:usesCleartextTraffic="true">
<!-- Your activities and other components -->
</application>
</manifest>iOS Configuration
1. iOS Permissions
CRITICAL: Add the following permissions to your ios/Runner/Info.plist. Missing NSMicrophoneUsageDescription will cause the app to crash when accessing the microphone.
<key>NSMicrophoneUsageDescription</key>
<string>This app needs access to microphone to enable voice functionality in the webview.</string>
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<false/>
<key>NSAllowsLocalNetworking</key>
<true/>
</dict>WebView Implementation
Permission Handling
The app uses a two-layer permission system:
import 'package:permission_handler/permission_handler.dart';
import 'package:flutter_inappwebview/flutter_inappwebview.dart';
Future<PermissionResponse> _requestMicrophonePermission(
List<PermissionResourceType> resources,
) async {
try {
// Check current permission status
PermissionStatus status = await Permission.microphone.status;
if (status.isDenied) {
// Request permission
status = await Permission.microphone.request();
if (status.isGranted) {
return PermissionResponse(
resources: resources,
action: PermissionResponseAction.GRANT,
);
}
} else if (status.isGranted) {
// Permission granted, allow WebView access
return PermissionResponse(
resources: resources,
action: PermissionResponseAction.GRANT,
);
}
// Permission denied
return PermissionResponse(
resources: resources,
action: PermissionResponseAction.DENY,
);
} catch (e) {
print('Permission request error: $e');
return PermissionResponse(
resources: resources,
action: PermissionResponseAction.DENY,
);
}
}WebView Configuration
The WebView is configured with specific settings to enable microphone access:
InAppWebView(
initialUrlRequest: URLRequest(
url: WebUri('https://your-website.com')
),
initialSettings: InAppWebViewSettings(
javaScriptEnabled: true,
mediaPlaybackRequiresUserGesture: false,
allowsInlineMediaPlayback: true,
allowsAirPlayForMediaPlayback: true,
allowsPictureInPictureMediaPlayback: true,
useHybridComposition: true,
mixedContentMode: MixedContentMode.MIXED_CONTENT_ALWAYS_ALLOW,
allowFileAccess: true,
allowContentAccess: true,
),
onPermissionRequest: (controller, request) async {
if (request.resources.contains(PermissionResourceType.MICROPHONE)) {
return await _requestMicrophonePermission(request.resources);
}
return PermissionResponse(
resources: request.resources,
action: PermissionResponseAction.DENY,
);
},
onLoadError: (controller, url, code, message) {
print('WebView load error: $code - $message');
},
onConsoleMessage: (controller, consoleMessage) {
print('Console: ${consoleMessage.message}');
},
)Usage Examples
Complete Implementation Example
import 'package:flutter/material.dart';
import 'package:flutter_inappwebview/flutter_inappwebview.dart';
import 'package:permission_handler/permission_handler.dart';
class MicrophoneWebView extends StatefulWidget {
final String targetUrl;
const MicrophoneWebView({
Key? key,
this.targetUrl = 'https://your-website.com',
}) : super(key: key);
@override
State<MicrophoneWebView> createState() => _MicrophoneWebViewState();
}
class _MicrophoneWebViewState extends State<MicrophoneWebView> {
InAppWebViewController? _webViewController;
bool _isLoading = true;
bool _micGranted = false;
@override
void initState() {
super.initState();
_requestMicrophonePermission();
}
Future<void> _requestMicrophonePermission() async {
try {
PermissionStatus status = await Permission.microphone.request();
setState(() {
_micGranted = status.isGranted;
});
} catch (e) {
print('Permission request failed: $e');
_showErrorDialog('Failed to request microphone permission');
}
}
Future<PermissionResponse> _handlePermissionRequest(
List<PermissionResourceType> resources,
) async {
if (resources.contains(PermissionResourceType.MICROPHONE)) {
if (_micGranted) {
return PermissionResponse(
resources: resources,
action: PermissionResponseAction.GRANT,
);
} else {
// Request permission again
await _requestMicrophonePermission();
return PermissionResponse(
resources: resources,
action: _micGranted
? PermissionResponseAction.GRANT
: PermissionResponseAction.DENY,
);
}
}
return PermissionResponse(
resources: resources,
action: PermissionResponseAction.DENY,
);
}
void _showErrorDialog(String message) {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('Error'),
content: Text(message),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('OK'),
),
],
),
);
}
@override
Widget build(BuildContext context) {
if (!_micGranted) {
return Scaffold(
appBar: AppBar(title: const Text('WebView')),
body: const Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.mic_off, size: 64, color: Colors.red),
SizedBox(height: 16),
Text(
'Microphone permission is required for voice features',
textAlign: TextAlign.center,
style: TextStyle(fontSize: 16),
),
],
),
),
);
}
return Scaffold(
appBar: AppBar(
title: const Text('WebView'),
actions: [
if (_isLoading)
const Center(
child: Padding(
padding: EdgeInsets.all(16.0),
child: SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(strokeWidth: 2),
),
),
),
],
),
body: InAppWebView(
initialUrlRequest: URLRequest(
url: WebUri(widget.targetUrl),
),
initialSettings: InAppWebViewSettings(
javaScriptEnabled: true,
mediaPlaybackRequiresUserGesture: false,
allowsInlineMediaPlayback: true,
useHybridComposition: true,
mixedContentMode: MixedContentMode.MIXED_CONTENT_ALWAYS_ALLOW,
),
onWebViewCreated: (controller) {
_webViewController = controller;
},
onPermissionRequest: (controller, request) async {
return await _handlePermissionRequest(request.resources);
},
onLoadStart: (controller, url) {
setState(() {
_isLoading = true;
});
},
onLoadStop: (controller, url) {
setState(() {
_isLoading = false;
});
},
onLoadError: (controller, url, code, message) {
print('WebView load error: $code - $message');
_showErrorDialog('Failed to load webpage: $message');
},
onConsoleMessage: (controller, consoleMessage) {
print('Console: ${consoleMessage.message}');
},
),
);
}
}Troubleshooting
Common Issues
Microphone permission denied
Check the following requirements:
- ✅ App has microphone permissions in device settings
- ✅ Manifest permissions are declared correctly
- ✅ Permission handler is implemented properly
- ✅ WebView permission delegation is configured
// Check permission status
Future<void> checkPermissionStatus() async {
PermissionStatus status = await Permission.microphone.status;
print('Microphone permission status: $status');
if (status.isPermanentlyDenied) {
// Open app settings
await openAppSettings();
}
}WebView not loading
Verify connectivity and configuration:
- ✅ Check internet connectivity
- ✅ Verify URL accessibility in browser
- ✅ Ensure JavaScript is enabled
- ✅ Check mixed content settings
// Add comprehensive error handling
onLoadError: (controller, url, code, message) {
print('Load error: $code - $message');
print('Failed URL: $url');
// Show user-friendly error
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Failed to load webpage')),
);
}JavaScript errors in WebView
Debug JavaScript execution issues:
- ✅ Verify JavaScript is enabled in WebView settings
- ✅ Check console messages for errors
- ✅ Test website in mobile browser first
- ✅ Ensure all required permissions are granted
// Monitor console messages
onConsoleMessage: (controller, consoleMessage) {
print('Console [${consoleMessage.messageLevel}]: ${consoleMessage.message}');
if (consoleMessage.messageLevel == ConsoleMessageLevel.ERROR) {
// Handle JavaScript errors
print('JavaScript error detected');
}
}Best Practices
Security & Permissions:
- Only grant necessary permissions to WebView
- Restrict WebView to trusted domains
- Always request user permission before accessing microphone
- Handle permission denials gracefully with user-friendly messages
Performance Optimization:
- Use lazy loading for WebView when possible
- Cache permission status to avoid repeated requests
- Implement proper error handling and loading states
- Monitor WebView memory usage and dispose properly
Configuration Management:
- Use environment variables for sensitive URLs
- Create configurable settings for different environments
- Test with various target websites during development
- Implement logging for debugging and monitoring
Support
For additional help:
- Email Support: contact@revrag.ai
Last Updated: June 2025