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.CAMERAManifest.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).