Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Select an option

  • Save bewithdhanu/78371f91f19c01d780c8083b62304a3e to your computer and use it in GitHub Desktop.

Select an option

Save bewithdhanu/78371f91f19c01d780c8083b62304a3e to your computer and use it in GitHub Desktop.
WebView JavaScript Communication Guide

Complete WebView JavaScript Communication Guide

Table of Contents

Overview

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.

Setup

Android Setup

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)
    }
}

iOS Setup

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)
    }
}

JavaScript to Native Communication

JavaScript Implementation

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;
    }
}

Android Implementation

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")
    }
}

iOS Implementation

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)")
    }
}

Data Types Examples

// 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 });

Native to JavaScript Communication

Android to JavaScript

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)
        }
    }
}

iOS to JavaScript

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)")
                }
            }
        }
    }
}

JavaScript Receiver

// 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');
}

Native Data Types Examples

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"
    ]
])

File Handling

Creating Files in JavaScript

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
    });
}

File Transfer Implementation

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)")
        }
    }
}

File Download Handling

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)
    }
}

Best Practices

  1. Error Handling

    • Implement comprehensive try-catch blocks
    • Provide meaningful error messages
    • Log errors with context
    • Handle network failures gracefully
  2. Data Validation

    • Validate data types before processing
    • Check for null/undefined values
    • Sanitize input data
    • Version check interfaces
  3. Performance

    • Minimize message size
    • Batch operations when possible
    • Handle large data in chunks
    • Use appropriate threading
  4. Security

    • Validate input data
    • Sanitize file names
    • Check file types
    • Implement proper permissions
    • Use HTTPS for web content
  5. Memory Management

    • Clean up resources
    • Handle WebView lifecycle
    • Monitor memory usage
    • Release unused handlers

Security Considerations

  1. File Operations

    • Validate file types before saving
    • Check file sizes
    • Scan for malicious content
    • Use appropriate permissions
    • Sanitize file names
  2. Data Transfer

    • Validate data structure
    • Check data size limits
    • Sanitize input data
    • Use secure protocols
  3. WebView Security

    • Enable minimal required permissions
    • Use content security policy
    • Validate URLs
    • Handle SSL certificates

Troubleshooting

  1. Common Issues

    • Interface not found
    • Data type mismatches
    • Threading issues
    • Memory leaks
    • Permission errors
  2. Debugging Tips

    • Use Chrome remote debugging for WebView
    • Enable console logging
    • Monitor network traffic
    • Check system logs
    • Validate data flow
  3. Performance Issues

    • Monitor message sizes
    • Check threading
    • Profile memory usage
    • Analyze network calls
  4. File Operations

    • Check storage permissions
    • Verify file paths
    • Monitor disk space
    • Validate file operations
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment