Para usar un WebView en Jetpack Compose, debes unirlo a un AndroidView.
En esta guía, se explican los casos de uso comunes y cómo admitirlos en Compose.
Une un WebView con AndroidView
Para usar un WebView en Compose, únelo con un AndroidView:
@Composable fun SimpleWebView( initialUrl: String, modifier: Modifier = Modifier ) { AndroidView( modifier = modifier.fillMaxSize(), factory = { context -> WebView(context).apply { webViewClient = WebViewClient() settings.javaScriptEnabled = true loadUrl(initialUrl) } } ) }
Esto funciona para mostrar una URL simple dentro de tu app. Sin embargo, WebView se ocupa de ciclos de vida de estado complejos que son independientes del ciclo de vida de la vista de Android y del ciclo de vida de Compose. La integración de Compose puede generar situaciones complejas de WebView que generen errores difíciles. En las siguientes secciones,
se describen los casos de uso que pueden requerir un manejo específico para admitir esas funciones.
Cómo conservar el estado de WebView
Controlar los cambios de configuración y la navegación en Compose es un desafío porque WebView es un View heredado vinculado a su Activity de host, y no se recomienda que su instancia sobreviva al ciclo de vida de Activity.
Por lo tanto, la forma estándar de conservar el estado de un WebView es permitir que las instancias de WebView se destruyan y se vuelvan a crear junto con el Activity. Puedes conservar de forma manual su historial de navegación interno y el estado de desplazamiento con un Bundle.
@Composable fun PersistentWebView(url: String) { val webViewStateBundle = rememberSaveable { Bundle() } AndroidView( factory = { context -> WebView(context).apply { webViewClient = WebViewClient() settings.javaScriptEnabled = true // Restore the state and history if (webViewStateBundle.containsKey("WEBVIEW_STATE")) { restoreState(webViewStateBundle.getBundle("WEBVIEW_STATE")!!) } else { loadUrl(url) } } }, onRelease = { releasedWebView -> // Save navigation history before the instance is destroyed val bundle = Bundle() releasedWebView.saveState(bundle) webViewStateBundle.putBundle("WEBVIEW_STATE", bundle) }, modifier = Modifier.fillMaxSize() ) }
Cómo controlar la navegación hacia atrás
Cuando un WebView tiene historial de navegación, el gesto de retroceso del sistema debe navegar hacia atrás dentro del WebView en lugar de salir de la pantalla.
Usa la API de Compose BackHandler para interceptar el evento de retroceso del sistema y llamar a la función WebView goBack():
// ... @Composable fun BackNavigationDemoScreen(onBack: () -> Unit) { // Hold a reference to the WebView to check its history state var webViewReference by remember { mutableStateOf<WebView?>(null) } // Intercept the system back press if the WebView has history BackHandler(enabled = true) { val webView = webViewReference if (webView != null && webView.canGoBack()) { webView.goBack() // Go back in history } else { onBack() // Exit screen } } Scaffold( topBar = { TopAppBar( title = { Text("Back Navigation Demo") }, navigationIcon = { IconButton(onClick = onBack) { Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Back") } } ) } ) { padding -> Column(modifier = Modifier.fillMaxSize().padding(padding)) { AndroidView( modifier = Modifier.fillMaxSize(), factory = { context -> WebView(context).apply { settings.javaScriptEnabled = true // Keeps link navigations internal to the WebView instead of opening Chrome webViewClient = WebViewClient() loadUrl("https://developer.android.com") webViewReference = this } }, onRelease = { webViewReference = null } ) } } }
Esta implementación proporciona un comportamiento de navegación similar al de un navegador.
Desplazamiento anidado
El desplazamiento anidado no se admite fácilmente cuando se usa WebView en Compose. Cuando
colocas un WebView dentro de un contenedor de Compose desplazable, como un
LazyColumn, el WebView puede consumir todos los gestos de desplazamiento.
Como WebView se basa en su propio motor de renderización interno, anidarlo con LazyColumn no funciona correctamente en este momento.
Para hacer un seguimiento del progreso de la compatibilidad oficial con el desplazamiento anidado para WebView, consulta este error.
Diseños de borde a borde e inserciones de ventanas
Cuando se usan diseños de borde a borde, el contenido de WebView puede aparecer debajo de las barras del sistema, como la barra de estado. Puedes usar el modificador windowInsetsPadding para enviar todo el WebView al área segura:
@Composable fun EdgeToEdgeDemo(url: String) { AndroidView( modifier = Modifier .fillMaxSize() .windowInsetsPadding(WindowInsets.systemBars), factory = { context -> WebView(context).apply { loadUrl(url) } } ) }
Para obtener más información sobre los elementos de inserción, consulta Información sobre los elementos de inserción de ventanas en WebView.
Cómo sincronizar el tema de la app con el contenido de WebView
Cuando la aplicación cambia entre el modo claro y el oscuro, el contenido de WebView se puede actualizar automáticamente sin volver a cargar la página si se controla de forma correcta.
Si eres el propietario del contenido de la página web, para sincronizar los colores con el tema de la app,
controla la consulta de medios prefers-color-scheme para asegurarte de que tu página web se adapte
al tema seleccionado.
Para habilitar elementos nativos, como menús desplegables y ventanas emergentes, para detectar y hacer coincidir el tema de tu app, aplica un tema de estilo DayNight a tu Activity.
<resources> <!-- ... <!-- Use a DayNight theme in your manifest to handle both modes automatically --> <style name="Theme.Webviewdemo.DayNight" parent="Theme.AppCompat.DayNight.NoActionBar" /> </resources>
@Composable fun ThemeSyncDemo(onBack: () -> Unit) { val context = LocalContext.current AndroidView( modifier = Modifier.fillMaxSize(), factory = { _ -> WebView(context).apply { settings.javaScriptEnabled = true webViewClient = WebViewClient() val html = """ <html> <head> // ... @media (prefers-color-scheme: dark) { body { background-color: #212121; color: #ffffff; } select { border-color: #BB86FC; background: #212121; color: #ffffff; } } </style> </head> // ... </html> """.trimIndent() loadDataWithBaseURL(null, html, "text/html", "UTF-8", null) } } ) }
Si la página web no tiene un tema oscuro o si no eres el propietario del contenido web, el oscurecimiento algorítmico puede ayudar a forzar un tema oscuro. Los sitios web modernos que ya tienen el modo oscuro ignoran este algoritmo y, en su lugar, usan sus propios estilos incorporados.
Cómo controlar los permisos web en Compose
Cuando una página web solicita acceso al hardware o a los datos (por ejemplo, la cámara, el micrófono o la ubicación), WebView activa devoluciones de llamada específicas en su WebChromeClient. Debes controlar estas devoluciones de llamada y asegurarte de que se otorguen los permisos de tiempo de ejecución de Android correspondientes.
Cómo controlar los permisos de la cámara y el micrófono
Cuando una página web solicita acceso a la cámara o al micrófono (por ejemplo, WebRTC o
grabación de video), WebView llama a WebChromeClient.onPermissionRequest.
Sin embargo, antes de llamar a grant(), debes solicitar los siguientes permisos de tiempo de ejecución de Android:
Manifest.permission.CAMERAManifest.permission.RECORD_AUDIO
Primero, define un controlador de permisos para WebView que realice un seguimiento de los PermissionRequest solicitados desde WebView:
class WebViewPermissionHandler( private val launcher: ManagedActivityResultLauncher<Array<String>, Map<String, Boolean>> ) { var pendingRequest by mutableStateOf<PermissionRequest?>(null) private set fun handleRequest(request: PermissionRequest) { val isTrustedOrigin = request.origin.host == "www.trusted-domain.com" || request.origin.host == "app.local" // Always verify the origin before granting request if (!isTrustedOrigin) { Log.w("WebViewPermission", "Blocked and denied permission request from untrusted origin: ${request.origin.host}") request.deny() return } val androidPermissions = mutableListOf<String>() request.resources.forEach { resource -> when (resource) { PermissionRequest.RESOURCE_VIDEO_CAPTURE -> androidPermissions.add(Manifest.permission.CAMERA) PermissionRequest.RESOURCE_AUDIO_CAPTURE -> androidPermissions.add(Manifest.permission.RECORD_AUDIO) } } // Save the request and launch the Android system dialog pendingRequest = request launcher.launch(androidPermissions.toTypedArray()) } fun onResult(results: Map<String, Boolean>) { val allGranted = results.values.all { it } Log.d("WebViewPermission", "Kotlin: All permissions granted? $allGranted") if (allGranted) { pendingRequest?.grant(arrayOf("/* list of permissions */")) } else { pendingRequest?.deny() } pendingRequest = null } }
A continuación, crea una función de componibilidad que recuerde el WebViewPermissionHandler. Usa
rememberLauncherForActivityResult para solicitar permisos:
@Composable fun rememberWebViewPermissionHandler(): WebViewPermissionHandler { val handlerState = remember { mutableStateOf<WebViewPermissionHandler?>(null) } val launcher = rememberLauncherForActivityResult( ActivityResultContracts.RequestMultiplePermissions() ) { results -> handlerState.value?.onResult(results) } return remember { WebViewPermissionHandler(launcher).also { handlerState.value = it } } }
Controla el permiso desde la devolución de llamada onPermissionRequest. Esto inicia el selector de permisos:
@Composable fun WebViewPermissionScreen() { val permissionHandler = rememberWebViewPermissionHandler() AndroidView( factory = { context -> WebView(context).apply { settings.javaScriptEnabled = true webChromeClient = object : WebChromeClient() { override fun onPermissionRequest(request: PermissionRequest) { // Simply delegate to the handler permissionHandler.handleRequest(request) } } // load a web page that needs permissions } }, modifier = Modifier.fillMaxSize() ) }
Alternativa a un WebView incorporado
Si prefieres evitar incorporar WebView, Android proporciona otras opciones para mostrar contenido web, como las pestañas personalizadas de Chrome. Consulta Cómo usar contenido web en tu app para Android para comprender cómo elegir el enfoque correcto para tus casos de uso (como la navegación o la autenticación).
Recursos adicionales
- Administra y diagnostica la memoria de WebView
- Administra el estado de WebView de manera eficiente
- Cómo depurar apps web