Incorporare una WebView in Compose

Per utilizzare un WebView in Jetpack Compose, devi racchiuderlo in un AndroidView. Questa guida spiega i casi d'uso comuni e come supportarli in Compose.

Eseguire il wrapping di una WebView con AndroidView

Per utilizzare un WebView in Compose, racchiudilo in 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)			
            }
        }
    )
}

Questo metodo funziona per mostrare un semplice URL all'interno dell'app. Tuttavia, WebView gestisce cicli di vita complessi degli stati separati dal ciclo di vita di Android View e dal ciclo di vita di Compose. L'integrazione di Compose può introdurre scenari complessi WebView che comportano bug difficili. Le sezioni seguenti descrivono i casi d'uso che potrebbero richiedere una gestione specifica per supportare queste funzionalità.

Mantieni lo stato di WebView

La gestione delle modifiche alla configurazione e della navigazione in Compose è difficile perché WebView è un View legacy associato al relativo Activity host ed è sconsigliato che la sua istanza sopravviva al ciclo di vita di Activity.

Pertanto, il modo standard per rendere persistente lo stato di un WebView consiste nel consentire la distruzione e la ricreazione delle istanze WebView insieme a Activity. Puoi conservare manualmente la cronologia di navigazione interna e lo stato di scorrimento utilizzando 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()
    )
}

Gestire la navigazione a ritroso

Quando un WebView ha una cronologia di navigazione, il gesto Indietro del sistema deve spostarsi all'interno del WebView anziché uscire dalla schermata.

Utilizza l'API Compose BackHandler per intercettare l'evento di ritorno del sistema e chiama la funzione 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
                }
            )
        }
    }
}

Questa implementazione fornisce un comportamento di navigazione in stile browser.

Scorrimento nidificato

Lo scorrimento nidificato non è supportato facilmente quando utilizzi WebView in Scrittura. Quando inserisci un WebView all'interno di un contenitore Compose scorrevole, ad esempio un LazyColumn, il WebView potrebbe consumare tutti i gesti di scorrimento. Poiché WebView si basa sul proprio motore di rendering interno, l'incorporamento con LazyColumn non funziona correttamente al momento.

Per monitorare l'avanzamento del supporto ufficiale dello scorrimento nidificato per WebView, consulta questo problema.

Layout edge-to-edge e rientri delle finestre

Quando utilizzi layout edge-to-edge, i contenuti WebView potrebbero essere visualizzati sotto le barre di sistema, ad esempio la barra di stato. Puoi utilizzare il modificatore windowInsetsPadding per spostare l'intero WebView nell'area sicura:

@Composable
fun EdgeToEdgeDemo(url: String) {
    AndroidView(
        modifier = Modifier
            .fillMaxSize()
            .windowInsetsPadding(WindowInsets.systemBars),
        factory = { context ->
            WebView(context).apply {
                loadUrl(url)
            }
        }
    )
}

Per saperne di più sugli inset, vedi Informazioni sugli inset delle finestre in WebView.

Sincronizzare il tema dell'app con i contenuti di WebView

Quando l'applicazione passa dalla modalità Chiaro alla modalità Buio, i contenuti WebView possono aggiornarsi automaticamente senza ricaricare la pagina se gestiti correttamente.

Se possiedi i contenuti della pagina web, per sincronizzare i colori con il tema dell'app, gestisci la media query prefers-color-scheme per assicurarti che la pagina web si adatti al tema selezionato.

Per consentire agli elementi nativi come i menu a discesa e i popup di rilevare e abbinare il tema dell'app, applica un tema di stile DayNight al tuo 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)
            }
        }
    )
} 

Se la pagina web non ha un tema scuro o se non possiedi i contenuti web, l'oscuramento algoritmico� può aiutarti a forzare un tema scuro. I siti web moderni che hanno già la modalità Buio ignorano questo algoritmo e utilizzano i propri stili integrati.

Gestire le autorizzazioni web in Compose

Quando una pagina web richiede l'accesso all'hardware o ai dati (ad esempio fotocamera, microfono o posizione), WebView attiva callback specifici nel suo WebChromeClient. Devi gestire questi callback e assicurarti che vengano concesse le autorizzazioni di runtime Android corrispondenti.

Gestire le autorizzazioni per fotocamera e microfono

Quando una pagina web richiede l'accesso alla fotocamera o al microfono (ad esempio WebRTC o registrazione video), WebView chiama WebChromeClient.onPermissionRequest.

Tuttavia, prima di chiamare grant(), devi richiedere le seguenti autorizzazioni di runtime di Android:

  • Manifest.permission.CAMERA
  • Manifest.permission.RECORD_AUDIO

Innanzitutto, definisci un gestore delle autorizzazioni per WebView che tiene traccia di PermissionRequest richieste da 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
    }
}

Successivamente, crea un composable che memorizzi WebViewPermissionHandler. Utilizza rememberLauncherForActivityResult per richiedere le autorizzazioni:

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

Gestisci l'autorizzazione dal callback onPermissionRequest. Viene avviato il launcher delle autorizzazioni:

@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 una WebView incorporata

Se preferisci evitare l'incorporamento di WebView, Android offre altre opzioni per visualizzare contenuti web, come le schede personalizzate di Chrome. Consulta Utilizzare i contenuti web all'interno dell'app per Android per capire come scegliere l'approccio corretto per i tuoi casi d'uso (come la navigazione o l'autenticazione).