- Overview
- Setup
- JavaScript to Native Communication
- Native to JavaScript Communication
- File Handling
- Best Practices
- Security Considerations
- Troubleshooting
This guide provides a comprehensive implementation of bidirectional communication between WebView JavaScript and native platforms (Android/iOS). It covers data transfer, file handling, and best practices for robust implementation.
class MainActivity : AppCompatActivity() {
private lateinit var webView: WebView
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
webView = WebView(this).apply {
settings.apply {
javaScriptEnabled = true
domStorageEnabled = true
allowFileAccess = true
}
addJavascriptInterface(JSApplication(this@MainActivity), "JSApplication")
}
setContentView(webView)
}
}class ViewController: UIViewController {
private var webView: WKWebView!
private var jsApplication: JSApplication!
override func viewDidLoad() {
super.viewDidLoad()
// Create JS Application handler
jsApplication = JSApplication()
// Setup WebView
let config = WKWebViewConfiguration()
let userContentController = WKUserContentController()
userContentController.add(jsApplication, name: "JSApplication")
config.userContentController = userContentController
webView = WKWebView(frame: view.bounds, configuration: config)
jsApplication.webView = webView
view.addSubview(webView)
}
}function sendEvent(event, ...data) {
if (!event || typeof event !== 'string') {
console.error('Event name is required and must be a string');
return false;
}
try {
// iOS
if (window?.webkit?.messageHandlers?.JSApplication) {
window.webkit.messageHandlers.JSApplication.postMessage({
event,
data
});
return true;
}
// Android
if (typeof JSApplication !== 'undefined') {
JSApplication.sendEvent(event, JSON.stringify(data));
return true;
}
console.warn('Native interface not found');
return false;
} catch (error) {
console.error('Failed to send event:', {
event,
data,
error: error.message
});
return false;
}
}class JSApplication(private val context: Context) {
@JavascriptInterface
fun sendEvent(event: String, dataJson: String) {
try {
val data = JSONArray(dataJson)
when (event) {
"userClick" -> {
val buttonId = data.getString(0)
handleUserClick(buttonId)
}
"formSubmit" -> {
val username = data.getString(0)
val email = data.getString(1)
val isValid = data.getBoolean(2)
handleFormSubmit(username, email, isValid)
}
"userUpdate" -> {
val userId = data.getString(0)
val userEmail = data.getString(1)
handleUserUpdate(userId, userEmail)
}
"profileUpdate" -> {
val profileData = JSONObject(data.getString(0))
val name = profileData.getString("name")
val age = profileData.getInt("age")
handleProfileUpdate(name, age)
}
}
} catch (e: Exception) {
Log.e("JSApplication", "Error processing event: ${e.message}")
}
}
private fun handleUserClick(buttonId: String) {
Log.d("JSApplication", "Button clicked: $buttonId")
}
private fun handleFormSubmit(username: String, email: String, isValid: Boolean) {
Log.d("JSApplication", "Form submitted: username=$username, email=$email, valid=$isValid")
}
private fun handleUserUpdate(userId: String, email: String) {
Log.d("JSApplication", "User updated: userId=$userId, email=$email")
}
private fun handleProfileUpdate(name: String, age: Int) {
Log.d("JSApplication", "Profile updated: name=$name, age=$age")
}
}class JSApplication: NSObject, WKScriptMessageHandler {
weak var webView: WKWebView?
func userContentController(_ userContentController: WKUserContentController,
didReceive message: WKScriptMessage) {
guard let dict = message.body as? [String: Any],
let event = dict["event"] as? String,
let data = dict["data"] as? [Any] else {
return
}
switch event {
case "userClick":
guard let buttonId = data[0] as? String else { return }
handleUserClick(buttonId: buttonId)
case "formSubmit":
guard let username = data[0] as? String,
let email = data[1] as? String,
let isValid = data[2] as? Bool else { return }
handleFormSubmit(username: username, email: email, isValid: isValid)
case "userUpdate":
guard let userId = data[0] as? String,
let userEmail = data[1] as? String else { return }
handleUserUpdate(userId: userId, email: userEmail)
case "profileUpdate":
guard let profileData = data[0] as? [String: Any],
let name = profileData["name"] as? String,
let age = profileData["age"] as? Int else { return }
handleProfileUpdate(name: name, age: age)
}
}
private func handleUserClick(buttonId: String) {
print("Button clicked: \(buttonId)")
}
private func handleFormSubmit(username: String, email: String, isValid: Bool) {
print("Form submitted: username=\(username), email=\(email), valid=\(isValid)")
}
private func handleUserUpdate(userId: String, email: String) {
print("User updated: userId=\(userId), email=\(email)")
}
private func handleProfileUpdate(name: String, age: Int) {
print("Profile updated: name=\(name), age=\(age)")
}
}// Single value
sendEvent('userClick', 'button1');
// Multiple values
sendEvent('formSubmit', 'john_doe', 'john@example.com', true);
// Array
const userData = ['user123', 'john@example.com'];
sendEvent('userUpdate', ...userData);
// Object
sendEvent('profileUpdate', { name: 'John', age: 30 });class JSApplication(private val webView: WebView) {
fun sendToJavaScript(event: String, data: Any) {
val jsonData = when (data) {
is String -> "\"$data\""
is Array<*>, is List<*> -> Gson().toJson(data)
else -> Gson().toJson(data)
}
val escapedJson = jsonData.replace("'", "\\'")
val jsCode = "javascript:window.onNativeEvent('$event', $escapedJson)"
webView.post {
webView.evaluateJavascript(jsCode, null)
}
}
}class JSApplication: NSObject {
weak var webView: WKWebView?
func sendToJavaScript(event: String, data: Any) {
guard let jsonData = try? JSONSerialization.data(withJSONObject: data),
let jsonString = String(data: jsonData, encoding: .utf8) else {
print("Failed to serialize data")
return
}
let jsCode = "window.onNativeEvent('\(event)', \(jsonString))"
DispatchQueue.main.async {
self.webView?.evaluateJavaScript(jsCode) { result, error in
if let error = error {
print("Error executing JavaScript: \(error)")
}
}
}
}
}// Setup native event handler
window.onNativeEvent = function(event, data) {
console.log('Received native event:', event, data);
switch(event) {
case 'userStatus':
handleUserStatus(data);
break;
case 'userData':
const [firstName, lastName, age] = data;
handleUserData(firstName, lastName, age);
break;
case 'userProfile':
handleUserProfile(data);
break;
case 'appConfig':
handleAppConfig(data);
break;
}
}
function handleUserStatus(status) {
console.log('User status:', status);
}
function handleUserData(firstName, lastName, age) {
console.log('User data:', { firstName, lastName, age });
}
function handleUserProfile(profile) {
console.log('User profile:', profile);
}
function handleAppConfig(config) {
console.log('App config:', config);
}
// Check handler initialization
if (!window.onNativeEvent) {
throw new Error('Native event handler not initialized');
}Android:
// String
jsApplication.sendToJavaScript("userStatus", "online")
// Array
jsApplication.sendToJavaScript("userData", listOf("John", "Doe", 30))
// Object
jsApplication.sendToJavaScript("userProfile", mapOf(
"name" to "John",
"age" to 30,
"email" to "john@example.com"
))
// Complex object
jsApplication.sendToJavaScript("appConfig", mapOf(
"theme" to "dark",
"notifications" to true,
"preferences" to mapOf(
"language" to "en",
"timezone" to "UTC"
)
))iOS:
// String
jsApplication.sendToJavaScript(event: "userStatus", data: "online")
// Array
jsApplication.sendToJavaScript(event: "userData", data: ["John", "Doe", 30])
// Object
jsApplication.sendToJavaScript(event: "userProfile", data: [
"name": "John",
"age": 30,
"email": "john@example.com"
])
// Complex object
jsApplication.sendToJavaScript(event: "appConfig", data: [
"theme": "dark",
"notifications": true,
"preferences": [
"language": "en",
"timezone": "UTC"
]
])function createAndSendFile() {
// Example: Creating a CSV
const csvContent = 'Name,Age\nJohn,30\nJane,25';
const blob = new Blob([csvContent], { type: 'text/csv' });
// Convert blob to base64
const reader = new FileReader();
reader.onloadend = function() {
const base64data = reader.result.split(',')[1];
sendEvent('fileCreated', {
filename: 'data.csv',
mimeType: 'text/csv',
data: base64data
});
};
reader.readAsDataURL(blob);
}
// Example: Creating PDF using pdfmake
function createAndSendPDF() {
const docDefinition = {
content: ['Hello World']
};
pdfMake.createPdf(docDefinition).getBase64(base64data => {
sendEvent('fileCreated', {
filename: 'document.pdf',
mimeType: 'application/pdf',
data: base64data
});
});
}
// Example: Creating Excel using SheetJS
function createAndSendExcel() {
const ws = XLSX.utils.json_to_sheet([
{ name: "John", age: 30 },
{ name: "Jane", age: 25 }
]);
const wb = XLSX.utils.book_new();
XLSX.utils.book_append_sheet(wb, ws, "Users");
const excelData = XLSX.write(wb, { bookType: 'xlsx', type: 'base64' });
sendEvent('fileCreated', {
filename: 'users.xlsx',
mimeType: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
data: excelData
});
}Android:
@JavascriptInterface
fun sendEvent(event: String, dataJson: String) {
try {
val data = JSONObject(dataJson)
when (event) {
"fileCreated" -> {
val filename = data.getString("filename")
val mimeType = data.getString("mimeType")
val base64Data = data.getString("data")
// Convert base64 to bytes
val fileBytes = android.util.Base64.decode(base64Data, android.util.Base64.DEFAULT)
// Save file
val downloads = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)
val file = File(downloads, filename)
FileOutputStream(file).use { output ->
output.write(fileBytes)
}
// Notify success
Handler(Looper.getMainLooper()).post {
Toast.makeText(context, "File saved: ${file.name}", Toast.LENGTH_SHORT).show()
}
// Notify system to scan the new file
MediaScannerConnection.scanFile(
context,
arrayOf(file.toString()),
arrayOf(mimeType)
) { path, uri -> }
}
}
} catch (e: Exception) {
Log.e("JSApplication", "Error processing event", e)
}
}iOS:
func userContentController(_ userContentController: WKUserContentController,
didReceive message: WKScriptMessage) {
guard let dict = message.body as? [String: Any],
let event = dict["event"] as? String,
let data = dict["data"] as? [String: Any] else {
return
}
switch event {
case "fileCreated":
guard let filename = data["filename"] as? String,
let mimeType = data["mimeType"] as? String,
let base64String = data["data"] as? String,
let fileData = Data(base64Encoded: base64String) else {
return
}
// Get documents directory
guard let documentsPath = FileManager.default.urls(for: .documentDirectory,
in: .userDomainMask).first else {
return
}
let fileUrl = documentsPath.appendingPathComponent(filename)
do {
// Save file
try fileData.write(to: fileUrl)
// Optional: Share file
let activityViewController = UIActivityViewController(
activityItems: [fileUrl],
applicationActivities: nil
)
// Present sharing options
DispatchQueue.main.async {
if let rootVC = UIApplication.shared.keyWindow?.rootViewController {
rootVC.present(activityViewController, animated: true)
}
}
} catch {
print("Error saving file: \(error)")
}
}
}Android:
class MainActivity : AppCompatActivity() {
private lateinit var webView: WebView
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
webView = WebView(this).apply {
settings.javaScriptEnabled = true
setDownloadListener { url, userAgent, contentDisposition, mimetype, contentLength ->
val request = DownloadManager.Request(Uri.parse(url)).apply {
val filename = URLUtil.guessFileName(url, contentDisposition, mimetype)
setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED)
setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, filename)
addRequestHeader("User-Agent", userAgent)
}
val downloadManager = getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
try {
downloadManager.enqueue(request)
Toast.makeText(this@MainActivity, "Downloading file...", Toast.LENGTH_SHORT).show()
} catch (e: Exception) {
Toast.makeText(this@MainActivity, "Download failed: ${e.message}", Toast.LENGTH_SHORT).show()
}
}
}
setContentView(webView)
}
// Optional: Monitor download progress
private val downloadReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
val id = intent?.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, -1)
if (id != -1L) {
val downloadManager = getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
val query = DownloadManager.Query().setFilterById(id)
val cursor = downloadManager.query(query)
if (cursor.moveToFirst()) {
val status = cursor.getInt(cursor.getColumnIndex(DownloadManager.COLUMN_STATUS))
when (status) {
DownloadManager.STATUS_SUCCESSFUL -> {
val localUri = cursor.getString(cursor.getColumnIndex(DownloadManager.COLUMN_LOCAL_URI))
// Handle successful download
}
DownloadManager.STATUS_FAILED -> {
// Handle failed download
}
}
}
cursor.close()
}
}
}
override fun onResume() {
super.onResume()
registerReceiver(
downloadReceiver,
IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE)
)
}
override fun onPause() {
super.onPause()
unregisterReceiver(downloadReceiver)
}
}iOS:
class ViewController: UIViewController, WKNavigationDelegate {
private var webView: WKWebView!
override func viewDidLoad() {
super.viewDidLoad()
let config = WKWebViewConfiguration()
webView = WKWebView(frame: view.bounds, configuration: config)
webView.navigationDelegate = self
view.addSubview(webView)
}
// Handle download request
func webView(_ webView: WKWebView,
decidePolicyFor navigationResponse: WKNavigationResponse,
decisionHandler: @escaping (WKNavigationResponsePolicy) -> Void) {
guard let url = navigationResponse.response.url,
let mimeType = navigationResponse.response.mimeType else {
decisionHandler(.allow)
return
}
if shouldDownload(mimeType: mimeType) {
decisionHandler(.cancel)
let session = URLSession.shared
let task = session.downloadTask(with: url) { (tempLocalUrl, response, error) in
guard let tempLocalUrl = tempLocalUrl else {
return
}
let filename = response?.suggestedFilename ?? url.lastPathComponent
guard let documentsPath = FileManager.default.urls(for: .documentDirectory,
in: .userDomainMask).first else {
return
}
let destinationUrl = documentsPath.appendingPathComponent(filename)
do {
if FileManager.default.fileExists(atPath: destinationUrl.path) {
try FileManager.default.removeItem(at: destinationUrl)
}
try FileManager.default.copyItem(at: tempLocalUrl, to: destinationUrl)
DispatchQueue.main.async {
self.showDownloadSuccess(filename: filename)
}
} catch {
DispatchQueue.main.async {
self.showDownloadError(error: error)
}
}
}
task.resume()
} else {
decisionHandler(.allow)
}
}
private func shouldDownload(mimeType: String) -> Bool {
let downloadTypes = [
"application/pdf",
"application/zip",
"application/msword",
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
// Add more mime types as needed
]
return downloadTypes.contains(mimeType)
}
private func showDownloadSuccess(filename: String) {
let alert = UIAlertController(
title: "Download Complete",
message: "File '\(filename)' has been downloaded successfully",
preferredStyle: .alert
)
alert.addAction(UIAlertAction(title: "OK", style: .default))
present(alert, animated: true)
}
private func showDownloadError(error: Error) {
let alert = UIAlertController(
title: "Download Failed",
message: error.localizedDescription,
preferredStyle: .alert
)
alert.addAction(UIAlertAction(title: "OK", style: .default))
present(alert, animated: true)
}
}-
Error Handling
- Implement comprehensive try-catch blocks
- Provide meaningful error messages
- Log errors with context
- Handle network failures gracefully
-
Data Validation
- Validate data types before processing
- Check for null/undefined values
- Sanitize input data
- Version check interfaces
-
Performance
- Minimize message size
- Batch operations when possible
- Handle large data in chunks
- Use appropriate threading
-
Security
- Validate input data
- Sanitize file names
- Check file types
- Implement proper permissions
- Use HTTPS for web content
-
Memory Management
- Clean up resources
- Handle WebView lifecycle
- Monitor memory usage
- Release unused handlers
-
File Operations
- Validate file types before saving
- Check file sizes
- Scan for malicious content
- Use appropriate permissions
- Sanitize file names
-
Data Transfer
- Validate data structure
- Check data size limits
- Sanitize input data
- Use secure protocols
-
WebView Security
- Enable minimal required permissions
- Use content security policy
- Validate URLs
- Handle SSL certificates
-
Common Issues
- Interface not found
- Data type mismatches
- Threading issues
- Memory leaks
- Permission errors
-
Debugging Tips
- Use Chrome remote debugging for WebView
- Enable console logging
- Monitor network traffic
- Check system logs
- Validate data flow
-
Performance Issues
- Monitor message sizes
- Check threading
- Profile memory usage
- Analyze network calls
-
File Operations
- Check storage permissions
- Verify file paths
- Monitor disk space
- Validate file operations