DEV Community

myougaTheAxo
myougaTheAxo

Posted on

WebView in Jetpack Compose: Web Content, JavaScript Bridge & Navigation

WebView in Jetpack Compose: Web Content, JavaScript Bridge & Navigation

Integrating web content into native Android apps is common for displaying web pages, progressive web apps, or server-rendered UI. Jetpack Compose handles this via AndroidView.

AndroidView Integration

Wrap WebView in a Compose AndroidView:

import android.webkit.WebView
import android.webkit.WebViewClient
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable
import androidx.compose.ui.Composable
import androidx.compose.ui.viewinterop.AndroidView

@Composable
fun WebViewScreen(url: String) {
    AndroidView(
        modifier = Modifier.fillMaxSize(),
        factory = { context ->
            WebView(context).apply {
                webViewClient = MyWebViewClient()
                settings.apply {
                    javaScriptEnabled = true
                    domStorageEnabled = true
                    databaseEnabled = true
                }
                loadUrl(url)
            }
        }
    )
}
Enter fullscreen mode Exit fullscreen mode

WebViewClient for Navigation Control

Override WebViewClient to control navigation behavior:

import android.webkit.WebViewClient
import android.webkit.WebResourceRequest

class MyWebViewClient : WebViewClient() {
    override fun shouldOverrideUrlLoading(
        view: WebView?,
        request: WebResourceRequest?
    ): Boolean {
        val url = request?.url.toString()

        // Allow internal URLs, block external
        return if (url.startsWith("https://myapp.com")) {
            false  // Allow default behavior
        } else {
            // Open external URLs in browser
            val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url))
            view?.context?.startActivity(intent)
            true  // Prevent WebView from loading
        }
    }

    override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) {
        super.onPageStarted(view, url, favicon)
        // Show loading indicator
    }

    override fun onPageFinished(view: WebView?, url: String?) {
        super.onPageFinished(view, url)
        // Hide loading indicator
    }
}
Enter fullscreen mode Exit fullscreen mode

addJavascriptInterface for Kotlin-JS Bridge

Enable bidirectional communication between Kotlin and JavaScript:

// In Kotlin
class JSBridge {
    @JavascriptInterface
    fun sendDataToWeb(message: String) {
        Log.d("JSBridge", "From JS: $message")
    }

    @JavascriptInterface
    fun updateUI(title: String, subtitle: String) {
        // Called from JavaScript to update native UI
        Log.d("JSBridge", "Title: $title, Subtitle: $subtitle")
    }
}

// Add bridge to WebView
webView.addJavascriptInterface(JSBridge(), "KotlinBridge")
Enter fullscreen mode Exit fullscreen mode

In JavaScript, call Kotlin functions:

// Invoke Kotlin function from JavaScript
KotlinBridge.sendDataToWeb("Hello from JavaScript!");
KotlinBridge.updateUI("New Title", "Updated Subtitle");
Enter fullscreen mode Exit fullscreen mode

loadDataWithBaseURL for HTML Strings

Load HTML content directly instead of from a URL:

val htmlContent = "<html><head>charset UTF-8</head><body>" +
    "<h1>Welcome</h1>" +
    "<p>This is local HTML</p>" +
    "</body></html>"

webView.loadDataWithBaseURL(
    null,
    htmlContent,
    "text/html",
    "utf-8",
    null
)
Enter fullscreen mode Exit fullscreen mode

BackHandler for Back Navigation

Handle the back button to navigate within WebView history:

import androidx.activity.compose.BackHandler

@Composable
fun WebViewScreen(url: String) {
    var webView: WebView? = null

    BackHandler {
        if (webView?.canGoBack() == true) {
            webView?.goBack()
        } else {
            // Pop back stack or exit
        }
    }

    AndroidView(
        factory = { context ->
            WebView(context).apply {
                webViewClient = MyWebViewClient()
                loadUrl(url)
            }.also { webView = it }
        }
    )
}
Enter fullscreen mode Exit fullscreen mode

Security Settings

Disable unnecessary features to reduce attack surface:

webView.settings.apply {
    // Disable file access (prevents local file system access)
    allowFileAccess = false

    // Disable local storage access
    allowFileAccessFromFileURLs = false
    allowUniversalAccessFromFileURLs = false

    // Domain whitelisting via WebViewClient
    javaScriptEnabled = true  // Only enable if needed
    domStorageEnabled = true

    // Disable mixed content (HTTP on HTTPS page)
    mixedContentMode = WebSettings.MIXED_CONTENT_NEVER_ALLOW
}
Enter fullscreen mode Exit fullscreen mode

Whitelist domains in WebViewClient.shouldOverrideUrlLoading():

private val allowedDomains = setOf(
    "myapp.com",
    "api.myapp.com",
    "cdn.myapp.com"
)

override fun shouldOverrideUrlLoading(
    view: WebView?,
    request: WebResourceRequest?
): Boolean {
    val url = request?.url.toString()
    val host = request?.url?.host ?: return true

    return if (host in allowedDomains) {
        false  // Allow
    } else {
        true   // Block
    }
}
Enter fullscreen mode Exit fullscreen mode

Complete Example: Compose + WebView + JS Bridge

@Composable
fun BlogViewerScreen(articleUrl: String) {
    var isLoading by remember { mutableStateOf(true) }
    var webView: WebView? = null

    BackHandler {
        if (webView?.canGoBack() == true) {
            webView?.goBack()
        }
    }

    Column(modifier = Modifier.fillMaxSize()) {
        if (isLoading) {
            CircularProgressIndicator()
        }

        AndroidView(
            modifier = Modifier
                .fillMaxSize()
                .weight(1f),
            factory = { context ->
                WebView(context).apply {
                    webViewClient = object : WebViewClient() {
                        override fun onPageFinished(view: WebView?, url: String?) {
                            isLoading = false
                        }
                    }
                    addJavascriptInterface(
                        object {
                            @JavascriptInterface
                            fun shareArticle(title: String) {
                                val shareIntent = Intent().apply {
                                    action = Intent.ACTION_SEND
                                    putExtra(Intent.EXTRA_TEXT, "$title at $articleUrl")
                                    type = "text/plain"
                                }
                                context.startActivity(shareIntent)
                            }
                        },
                        "NativeAPI"
                    )
                    settings.apply {
                        javaScriptEnabled = true
                        allowFileAccess = false
                        mixedContentMode = WebSettings.MIXED_CONTENT_NEVER_ALLOW
                    }
                    loadUrl(articleUrl)
                }.also { webView = it }
            }
        )
    }
}
Enter fullscreen mode Exit fullscreen mode

Learn More:

8 Android App Templates → https://myougatheax.gumroad.com

Top comments (0)