Encapsular uma WebView no Compose

Para usar um WebView no Jetpack Compose, é necessário envolvê-lo em um AndroidView. Este guia explica casos de uso comuns e como oferecer suporte a eles no Compose.

Encapsular uma WebView com AndroidView

Para usar um WebView no Compose, envolva-o com um 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)			
            }
        }
    )
}

Isso funciona para mostrar um URL simples no seu app. No entanto, o WebView lida com ciclos de vida de estado complexos que são separados do ciclo de vida do Android View e do ciclo de vida do Compose. A integração do Compose pode introduzir cenários complexos de WebView que resultam em bugs difíceis. As seções a seguir descrevem casos de uso que podem precisar de um tratamento específico para oferecer suporte a esses recursos.

Persistir o estado da WebView

Processar mudanças de configuração e navegação no Compose é um desafio porque o WebView é um View legado vinculado ao Activity host, e não é recomendável que a instância dele sobreviva ao ciclo de vida do Activity.

Portanto, a maneira padrão de manter o estado de um WebView é permitir que as instâncias de WebView sejam destruídas e recriadas junto com o Activity. É possível manter manualmente o histórico de navegação interna e o estado de rolagem usando um 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()
    )
}

Gerenciar a navegação de retorno

Quando um WebView tem histórico de navegação, o gesto de retorno do sistema deve navegar para trás no WebView em vez de sair da tela.

Use a API BackHandler do Compose para interceptar o evento de retorno do sistema e chame a função 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
                }
            )
        }
    }
}

Essa implementação fornece um comportamento de navegação no estilo do navegador.

Rolagem aninhada

A rolagem aninhada não é facilmente compatível ao usar WebView no Compose. Ao colocar um WebView dentro de um contêiner rolável do Compose, como um LazyColumn, o WebView pode consumir todos os gestos de rolagem. Como WebView depende do próprio mecanismo de renderização interno, o aninhamento com LazyColumn não funciona corretamente no momento.

Para acompanhar o progresso do suporte oficial à rolagem aninhada para WebView, consulte este problema.

Layouts de ponta a ponta e encartes de janela

Ao usar layouts de ponta a ponta, o conteúdo WebView pode aparecer abaixo das barras de sistema, como a barra de status. Use o modificador windowInsetsPadding para inserir todo o WebView na área segura:

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

Para mais informações sobre encartes, consulte Entender encartes de janela no WebView.

Sincronizar o tema do app com o conteúdo da WebView

Quando o aplicativo alterna entre o modo claro e o escuro, o conteúdo WebView pode ser atualizado automaticamente sem recarregar a página se for processado corretamente.

Se você for o proprietário do conteúdo da página da Web, para sincronizar as cores com o tema do app, processe a consulta de mídia prefers-color-scheme para garantir que a página da Web se adapte ao tema selecionado.

Para permitir que elementos nativos, como menus suspensos e pop-ups, detectem e correspondam ao tema do seu app, aplique um tema de estilo DayNight ao seu 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 a página da Web não tiver um tema escuro ou se você não for proprietário do conteúdo da Web, o escurecimento algorítmico pode ajudar a forçar um tema escuro. Sites modernos que já têm o modo escuro ignoram esse algoritmo e usam os próprios estilos integrados.

Processar permissões da Web no Compose

Quando uma página da Web solicita acesso a hardware ou acesso aos dados (por exemplo, câmera, microfone ou localização), o WebView aciona callbacks específicos no WebChromeClient. Você precisa processar esses callbacks e garantir que as permissões de execução do Android correspondentes sejam concedidas.

Gerenciar permissões de câmera e microfone

Quando uma página da Web solicita acesso à câmera ou ao microfone (por exemplo, WebRTC ou gravação de vídeo), o WebView chama o WebChromeClient.onPermissionRequest.

No entanto, antes de chamar grant(), você precisa solicitar as seguintes permissões de tempo de execução do Android:

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

Primeiro, defina um gerenciador de permissões para WebView que acompanhe o PermissionRequest solicitado de 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
    }
}

Em seguida, crie um elemento combinável que se lembre do WebViewPermissionHandler. Use rememberLauncherForActivityResult para solicitar permissões:

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

Processe a permissão do callback onPermissionRequest. Isso inicia o iniciador de permissões:

@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 uma WebView incorporada

Se você preferir evitar a incorporação de WebView, o Android oferece outras opções para mostrar conteúdo da Web, como as guias personalizadas do Chrome. Consulte Usar conteúdo da Web no seu app Android para entender como escolher a abordagem correta para seus casos de uso (como navegação ou autenticação).