Incorporare una WebView in Compose

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

Aggrega una WebView con AndroidView

Per utilizzare un WebView in Compose, inseriscilo 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 funziona per mostrare un semplice URL all'interno della tua app. Tuttavia, WebView gestisce cicli di vita di stato complessi che sono separati dal ciclo di vita della visualizzazione Android e dal ciclo di vita di Compose. L'integrazione di Compose può introdurre scenari WebView complessi che generano 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 host Activity e non è consigliabile che la sua istanza sopravviva al ciclo di vita di Activity.

Pertanto, il modo standard per rendere persistente lo stato di un WebView è consentire l'eliminazione e la ricreazione delle istanze WebView insieme a Activity. Puoi rendere persistente 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()
    )
}

Gestisci la navigazione a ritroso

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

Utilizza l'API Compose BackHandler per intercettare l'evento Indietro di 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 simile a quello del browser.

Scorrimento nidificato

Lo scorrimento nidificato non è facilmente supportato quando si utilizza WebView in Compose. Quando posizioni 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'annidamento con LazyColumn al momento non funziona correttamente.

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

Layout edge-to-edge e inset delle finestre

Quando utilizzi layout edge-to-edge, i contenuti di WebView potrebbero essere visualizzati sotto le barre di sistema, come la barra di stato. Puoi utilizzare il modificatore windowInsetsPadding per inserire 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, consulta Informazioni sugli inset delle finestre in WebView.

Sincronizza il tema dell'app con i contenuti WebView

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

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

Per consentire a elementi nativi come menu a discesa e popup di rilevare e abbinare il tema della tua 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 sei il proprietario dei contenuti web, l'oscuramento algoritmico può aiutare a forzare un tema scuro. I siti web moderni che hanno già la modalità Buio ignorano questo algoritmo e utilizzano i propri stili integrati.

Gestisci 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.

Gestisci le autorizzazioni di accesso alla fotocamera e al 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 Android:

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

Innanzitutto, definisci un gestore delle autorizzazioni per WebView che tenga traccia delle 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
    }
}

Poi, crea un componibile 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. In questo modo viene avviato il programma di avvio 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 di incorporare WebView, Android offre altre opzioni per visualizzare i contenuti web, come le schede personalizzate di Chrome. Consulta Utilizza i contenuti web all'interno della tua app per Android per capire come scegliere l'approccio corretto per i tuoi casi d'uso (ad esempio navigazione o autenticazione).

Risorse aggiuntive