WebView in Jetpack Compose: A Beginner-Friendly Tutorial
If you're building an Android app with Jetpack Compose and want to display a website inside your app, you might be asking: "Where is the WebView in Compose?" Jetpack Compose doesn’t offer a native WebView yet — but don't worry. In this tutorial, you'll learn how to integrate the traditional WebView
in your Compose app using AndroidView
.
✅ What You’ll Learn
- How to add WebView in Jetpack Compose
- Enable JavaScript support
- Support file upload from web pages
- Handle back button navigation
👉Step 1: Set Up Your Project
Create a new project in Android Studio using the Empty Compose Activity template. Then open your AndroidManifest.xml
and add this permission:
<uses-permission android:name="android.permission.INTERNET" />
This lets your app access websites from the internet.
👉Step 2: Add WebView Using AndroidView
Jetpack Compose provides a helper called AndroidView
to use classic Android views like WebView
.
@Composable
fun SimpleWebView(urlToRender: String) {
AndroidView(factory = { context ->
WebView(context).apply {
layoutParams = ViewGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT
)
webViewClient = WebViewClient() // open links inside app
loadUrl(urlToRender)
}
})
}
- AndroidView: Allows using classic views like WebView inside Compose.
- WebViewClient: Ensures links open inside the WebView, not in a browser.
- loadUrl: Loads the web page URL.
Use it in your MainActivity.kt
like this:
setContent {
MaterialTheme {
WebViewScreen(url = "https://developer.android.com/")
}
}
⚙️ Step 3: Enable JavaScript
Most websites today need JavaScript. So don’t forget to enable it:
WebView(context).apply {
settings.javaScriptEnabled = true // ✅ Enable JavaScript
webViewClient = WebViewClient()
loadUrl(urlToRender)
}
👉Step 4: File Upload Support
If your webpage has a file input, use this Composable with ActivityResultLauncher
:
var filePathCallback: ValueCallback>? = null
val filePickerLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.GetContent()
) { uri ->
uri?.let {
filePathCallback?.onReceiveValue(arrayOf(it))
filePathCallback = null
}
}
AndroidView(factory = { context ->
WebView(context).apply {
settings.javaScriptEnabled = true
webViewClient = WebViewClient()
webChromeClient = object : WebChromeClient() {
override fun onShowFileChooser(
view: WebView?,
filePathCallbackNew: ValueCallback>?,
fileChooserParams: FileChooserParams?
): Boolean {
filePathCallback = filePathCallbackNew
filePickerLauncher.launch("*/*")
return true
}
}
loadUrl(urlToRender)
}
})
- onShowFileChooser: Called when <input type="file" /> is triggered.
- filePickerLauncher: Opens system file picker and returns the file to the WebView.
👉Step 5: Handle Back Button
Let’s use BackHandler
to make the system back button work like a browser’s back button:
val webView = remember { WebView(LocalContext.current) }
BackHandler(enabled = webView.canGoBack()) {
webView.goBack()
}
- BackHandler: A Compose utility that overrides the system back button.
- webView.canGoBack(): Checks if the user has navigated inside WebView.
- webView.goBack(): Goes to the previous page.
✅ Final Tips
- Use HTTPS URLs only
- Enable JavaScript only when necessary
- Don’t store passwords or auto-fill in sensitive WebViews
👇Full working WebView code in Jetpack Compose with:
MainActivity
package com.example.codingcompose
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import com.example.codingcompose.ui.theme.CodingComposeTheme
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContent {
CodingComposeTheme {
/*Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
Greeting(
name = "Android",
modifier = Modifier.padding(innerPadding)
)
}*/
WebViewScreen(url = "https://developer.android.com/")
}
}
}
}
WebViewScreen
package com.example.codingcompose
import android.annotation.SuppressLint
import android.net.Uri
import android.view.ViewGroup
import android.webkit.ValueCallback
import android.webkit.WebChromeClient
import android.webkit.WebResourceError
import android.webkit.WebResourceRequest
import android.webkit.WebView
import android.webkit.WebViewClient
import androidx.activity.ComponentActivity
import androidx.activity.compose.BackHandler
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.systemBarsPadding
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.viewinterop.AndroidView
@SuppressLint("SetJavaScriptEnabled")
@Composable
fun WebViewScreen(url: String) {
val context = LocalContext.current
val webView = remember { WebView(context) }
var isLoading by remember { mutableStateOf(true) }
var hasError by remember { mutableStateOf(false) }
var filePathCallback: ValueCallback>? by remember { mutableStateOf(null) }
val filePickerLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.GetContent()
) { uri ->
filePathCallback?.onReceiveValue(uri?.let { arrayOf(it) })
filePathCallback = null
}
Box(modifier = Modifier.fillMaxSize()
.systemBarsPadding()) {
AndroidView(
factory = {
webView.apply {
layoutParams = ViewGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT
)
settings.domStorageEnabled = true
settings.javaScriptEnabled = true
webViewClient = object : WebViewClient() {
override fun onPageFinished(view: WebView?, url: String?) {
isLoading = false
hasError = false
}
override fun onReceivedError(
view: WebView?,
request: WebResourceRequest?,
error: WebResourceError?
) {
hasError = true
}
}
webChromeClient = object : WebChromeClient() {
override fun onShowFileChooser(
view: WebView?,
filePathCallbackNew: ValueCallback>?,
fileChooserParams: FileChooserParams?
): Boolean {
filePathCallback = filePathCallbackNew
filePickerLauncher.launch("*/*")
return true
}
}
loadUrl(url)
}
},
update = { it.loadUrl(url) },
modifier = Modifier.fillMaxSize()
)
// Loading Spinner
if (isLoading) {
Box(
modifier = Modifier
.fillMaxSize()
.background(MaterialTheme.colorScheme.background.copy(alpha = 0.6f)),
contentAlignment = Alignment.Center
) {
CircularProgressIndicator()
}
}
// Error Message
if (hasError) {
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
Text("⚠️ Failed to load the page.", color = MaterialTheme.colorScheme.error)
}
}
}
// Back button handler
BackHandler {
if (webView.canGoBack()) {
webView.goBack()
} else {
// Exit the app if no more history
(context as? ComponentActivity)?.finish()
}
}
}
📌 Summary
Jetpack Compose doesn’t yet have a built-in WebView, but you can easily integrate it with AndroidView
. You’ve now learned how to:
- Add WebView to Compose
- Enable JavaScript
- Support file inputs
- Implement back button support
This makes your app powerful enough to load and interact with modern websites — directly from your Compose UI!