Encapsuler une WebView dans Compose

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

Encapsuler un 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. Toutefois, WebView gère des cycles de vie d'état complexes qui sont distincts du cycle de vie d'Android View 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. Les sections suivantes décrivent les cas d'utilisation qui peuvent nécessiter une gestion spécifique pour prendre en charge ces fonctionnalités.

Persister l'état WebView

La gestion des modifications de configuration et de la navigation dans Compose est difficile, car WebView est une View héritée liée à son Activity hôte, et il est déconseillé que son instance survive au cycle de vie de la Activity.

Par conséquent, la méthode standard pour persister l'état d'un WebView consiste à autoriser la destruction et la recréation des instances WebView avec l'Activity. Vous pouvez persister 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 possède un historique de navigation, le geste de retour du système doit revenir en arrière dans le WebView plutôt que de quitter l'écran.

Utilisez l'API Compose BackHandler pour intercepter l'événement de 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 pris en charge lorsque vous utilisez WebView dans Compose. Lorsque vous placez un WebView dans un conteneur Compose déroulant, tel qu'un LazyColumn, le WebView peut consommer tous les gestes de défilement. Étant donné que WebView s'appuie sur son propre moteur de rendu interne, l'imbrication avec LazyColumn ne fonctionne pas correctement pour le moment.

Pour suivre la progression de la prise en charge 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 bord à bord, le contenu WebView peut apparaître sous les barres système, telles que la barre d'état. Vous pouvez utiliser le modificateur windowInsetsPadding pour insérer l'ensemble du WebView dans la zone sécurisée :

@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 passe du mode clair au mode sombre, le contenu WebView peut se mettre à jour automatiquement sans recharger la page s'il est géré correctement.

Si vous possédez le 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 pour vous assurer que votre page Web s'adapte au thème sélectionné.

Pour permettre aux éléments natifs tels que les listes déroulantes et les pop-up 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 n'a pas de thème sombre ou si vous ne possédez pas le 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 un accès 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 à la caméra et au micro

Lorsqu'une page Web demande l'accès à la caméra 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 suit le 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 à un WebView intégré

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 comprendre comment choisir l'approche appropriée pour vos cas d'utilisation (comme la navigation ou l'authentification).

Ressources supplémentaires