Encapsuler une WebView dans Compose

Pour utiliser un WebView dans Jetpack Compose, vous devez l'encapsuler dans un AndroidView. Ce guide présente les cas d'utilisation courants et explique comment les prendre en charge dans Compose.

Encapsuler une WebView avec AndroidView

Pour utiliser un WebView dans Compose, encapsulez-le avec 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)			
            }
        }
    )
}

Cela fonctionne pour afficher une URL simple dans votre application. Cependant, WebView gère des cycles de vie d'état complexes qui sont distincts du cycle de vie de la vue Android et du cycle de vie de Compose. L'intégration de Compose peut introduire des scénarios WebView complexes qui entraînent des bugs difficiles à résoudre. Les sections suivantes décrivent les cas d'utilisation qui peuvent nécessiter une gestion spécifique pour prendre en charge ces fonctionnalités.

Conserver l'état de WebView

La gestion des modifications de configuration et de la navigation dans Compose est difficile, car WebView est un View ancien lié à son Activity hôte, et il n'est pas recommandé que son instance survive au cycle de vie de Activity.

Par conséquent, la méthode standard pour conserver l'état d'un WebView consiste à autoriser la destruction et la recréation des instances WebView avec le Activity. Vous pouvez conserver manuellement son historique de navigation interne et son état de défilement à l'aide d'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()
    )
}

Gérer la navigation vers l'arrière

Lorsqu'un WebView dispose d'un historique de navigation, le geste de retour du système doit permettre de revenir en arrière dans le WebView plutôt que de quitter l'écran.

Utilisez l'API Compose BackHandler pour intercepter l'événement "Retour" du système et appelez la fonction 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
                }
            )
        }
    }
}

Cette implémentation fournit un comportement de navigation de type navigateur.

Défilement imbriqué

Le défilement imbriqué n'est pas facilement compatible avec WebView dans Compose. Lorsque vous placez un WebView dans un conteneur Compose à défilement, tel qu'un LazyColumn, le WebView peut consommer tous les gestes de défilement. Comme WebView s'appuie sur son propre moteur de rendu interne, l'imbriquer avec LazyColumn ne fonctionne pas correctement pour le moment.

Pour suivre la progression de la compatibilité officielle du défilement imbriqué pour WebView, consultez ce problème.

Mises en page bord à bord et encarts de fenêtre

Lorsque vous utilisez des mises en page de bord à bord, le contenu WebView peut apparaître sous les barres système, comme la barre d'état. Vous pouvez utiliser le modificateur windowInsetsPadding pour pousser l'intégralité de WebView dans la zone de sécurité :

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

Pour en savoir plus sur les encarts, consultez Comprendre les encarts de fenêtre dans WebView.

Synchroniser le thème de l'application avec le contenu WebView

Lorsque l'application bascule entre le mode clair et le mode sombre, le contenu WebView peut se mettre à jour automatiquement sans recharger la page s'il est géré correctement.

Si vous êtes propriétaire du contenu de la page Web, pour synchroniser les couleurs avec le thème de l'application, gérez la requête média prefers-color-scheme afin de vous assurer que votre page Web s'adapte au thème sélectionné.

Pour permettre aux éléments natifs tels que les menus déroulants et les pop-ups de détecter et de correspondre au thème de votre application, appliquez un thème de style DayNight à votre 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 page Web ne dispose pas d'un thème sombre ou si vous n'êtes pas propriétaire du contenu Web, l'assombrissement algorithmique peut vous aider à forcer un thème sombre. Les sites Web modernes qui disposent déjà d'un mode sombre ignorent cet algorithme et utilisent leurs propres styles intégrés.

Gérer les autorisations Web dans Compose

Lorsqu'une page Web demande l'accès au matériel ou aux données (par exemple, à l'appareil photo, au micro ou à la position), WebView déclenche des rappels spécifiques dans son WebChromeClient. Vous devez gérer ces rappels et vous assurer que les autorisations d'exécution Android correspondantes sont accordées.

Gérer les autorisations d'accès à l'appareil photo et au micro

Lorsqu'une page Web demande l'accès à l'appareil photo ou au micro (par exemple, WebRTC ou enregistrement vidéo), WebView appelle WebChromeClient.onPermissionRequest.

Toutefois, avant d'appeler grant(), vous devez demander les autorisations d'exécution Android suivantes :

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

Commencez par définir un gestionnaire d'autorisations pour WebView qui effectue le suivi de l'PermissionRequest demandé à partir 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
    }
}

Ensuite, créez un composable qui mémorise le WebViewPermissionHandler. Utilisez rememberLauncherForActivityResult pour demander des autorisations :

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

Gérez l'autorisation à partir du rappel onPermissionRequest. Le lanceur d'autorisations s'ouvre :

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

Alternative à une WebView intégrée

Si vous préférez éviter d'intégrer WebView, Android propose d'autres options pour afficher du contenu Web, comme les onglets personnalisés Chrome. Consultez Utiliser du contenu Web dans votre application Android pour savoir comment choisir l'approche adaptée à vos cas d'utilisation (comme la navigation ou l'authentification).

Ressources supplémentaires