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)
}
}
)
}
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
}
}
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")
In JavaScript, call Kotlin functions:
// Invoke Kotlin function from JavaScript
KotlinBridge.sendDataToWeb("Hello from JavaScript!");
KotlinBridge.updateUI("New Title", "Updated Subtitle");
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
)
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 }
}
)
}
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
}
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
}
}
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 }
}
)
}
}
Learn More:
8 Android App Templates → https://myougatheax.gumroad.com
Top comments (0)