Skip to main content

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.1
Then run:
flutter pub get

Solution Overview

This project implements a Flutter WebView solution using the flutter_inappwebview package with proper microphone permission handling. The solution:
  1. Requests native microphone permissions using the permission_handler package
  2. Delegates permissions to the WebView when the embed agent requests microphone access
  3. Handles permission states across both native and WebView contexts
  4. 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

Check the following requirements:
  1. ✅ App has microphone permissions in device settings
  2. ✅ Manifest permissions are declared correctly
  3. ✅ Permission handler is implemented properly
  4. ✅ 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();
  }
}
Verify connectivity and configuration:
  1. ✅ Check internet connectivity
  2. ✅ Verify URL accessibility in browser
  3. ✅ Ensure JavaScript is enabled
  4. ✅ 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')),
  );
}
Debug JavaScript execution issues:
  1. ✅ Verify JavaScript is enabled in WebView settings
  2. ✅ Check console messages for errors
  3. ✅ Test website in mobile browser first
  4. ✅ 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:
Last Updated: June 2025