WebView in Compose einbinden

Wenn Sie eine WebView in Jetpack Compose verwenden möchten, müssen Sie sie in eine AndroidView einbinden. In diesem Leitfaden werden gängige Anwendungsfälle und deren Unterstützung in Compose erläutert.

WebView mit AndroidView umschließen

Wenn Sie einen WebView in Compose verwenden möchten, umschließen Sie ihn mit einem 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)			
            }
        }
    )
}

Das funktioniert, wenn eine einfache URL in der App angezeigt werden soll. WebView befasst sich jedoch mit komplexen Statuslebenszyklen, die vom Android View-Lebenszyklus und vom Compose-Lebenszyklus getrennt sind. Die Einbindung von Compose kann zu komplexen WebView-Szenarien führen, die schwer zu behebende Fehler verursachen. In den folgenden Abschnitten werden Anwendungsfälle beschrieben, die möglicherweise eine spezielle Handhabung erfordern, um diese Funktionen zu unterstützen.

WebView-Status beibehalten

Der Umgang mit Konfigurationsänderungen und der Navigation in Compose ist eine Herausforderung, da WebView eine Legacy-View ist, die an ihren Host Activity gebunden ist. Es wird nicht empfohlen, dass die Instanz den Lebenszyklus von Activity überdauert.

Die Standardmethode zum Beibehalten des Status eines WebView besteht daher darin, zuzulassen, dass WebView-Instanzen zusammen mit dem Activity gelöscht und neu erstellt werden. Sie können den internen Navigationsverlauf und den Scrollstatus manuell mit einem Bundle beibehalten.

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

Rückwärtsnavigation verarbeiten

Wenn ein WebView einen Navigationsverlauf hat, sollte die Systemgeste „Zurück“ innerhalb des WebView zurückgehen, anstatt den Bildschirm zu verlassen.

Verwenden Sie die Compose API BackHandler, um das Systemereignis „Zurück“ abzufangen, und rufen Sie die Funktion WebView goBack() auf:

// ...
@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
                }
            )
        }
    }
}

Diese Implementierung bietet ein Navigationsverhalten wie im Browser.

Verschachteltes Scrollen

Verschachteltes Scrollen wird bei Verwendung von WebView in Compose nicht ohne Weiteres unterstützt. Wenn Sie eine WebView in einem scrollbaren Compose-Container wie einem LazyColumn platzieren, werden möglicherweise alle Scroll-Gesten von der WebView verarbeitet. Da WebView auf einer eigenen internen Rendering-Engine basiert, funktioniert die Verschachtelung mit LazyColumn derzeit nicht richtig.

Informationen zum Fortschritt der offiziellen Unterstützung für verschachteltes Scrollen für WebView finden Sie in diesem Problem.

Randlose Layouts und Fenster-Insets

Bei randlosen Layouts können WebView-Inhalte unter Systemleisten wie der Statusleiste angezeigt werden. Mit dem Modifikator windowInsetsPadding können Sie die gesamte WebView in den sicheren Bereich verschieben:

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

Weitere Informationen zu Insets finden Sie unter Fenster-Insets in WebView.

App-Design mit WebView-Inhalten synchronisieren

Wenn die Anwendung zwischen dem hellen und dem dunklen Modus wechselt, können WebView-Inhalte automatisch aktualisiert werden, ohne dass die Seite neu geladen werden muss, sofern dies korrekt gehandhabt wird.

Wenn Sie der Inhaber des Webseiteninhalts sind, können Sie die Farben mit dem Design der App synchronisieren. Verarbeiten Sie dazu die Medienabfrage prefers-color-scheme, damit sich Ihre Webseite an das ausgewählte Design anpasst.

Damit native Elemente wie Drop-down-Menüs und Pop-ups das Design Ihrer App erkennen und sich daran anpassen können, wenden Sie ein DayNight-Design auf Ihre Activity. an.

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

Wenn die Webseite kein dunkles Design hat oder Sie nicht der Inhaber des Webinhalts sind, kann die algorithmische Abdunklung helfen, ein dunkles Design zu erzwingen. Moderne Websites, die bereits einen dunklen Modus haben, ignorieren diesen Algorithmus und verwenden stattdessen ihre eigenen integrierten Stile.

Webberechtigungen in Compose verwalten

Wenn eine Webseite Zugriff auf Hardware oder Daten anfordert (z. B. Kamera, Mikrofon oder Standort), löst WebView bestimmte Callbacks in der WebChromeClient aus. Sie müssen diese Callbacks verarbeiten und dafür sorgen, dass die entsprechenden Android-Laufzeitberechtigungen gewährt werden.

Kamera- und Mikrofonberechtigungen verwalten

Wenn eine Webseite Zugriff auf die Kamera oder das Mikrofon anfordert (z. B. WebRTC oder Videoaufzeichnung), ruft WebView WebChromeClient.onPermissionRequest auf.

Bevor Sie grant() aufrufen, müssen Sie jedoch die folgenden Android-Laufzeitberechtigungen anfordern:

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

Definieren Sie zuerst einen Berechtigungshandler für WebView, der die von WebView angeforderten PermissionRequest im Blick behält:

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

Erstellen Sie als Nächstes eine zusammensetzbare Funktion, die sich den WebViewPermissionHandler merkt. Verwenden Sie rememberLauncherForActivityResult, um Berechtigungen anzufordern:

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

Verarbeite die Berechtigung über den onPermissionRequest-Callback. Dadurch wird der Berechtigungs-Launcher gestartet:

@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 zu einer eingebetteten WebView

Wenn Sie WebView nicht einbetten möchten, bietet Android andere Optionen zum Anzeigen von Webinhalten, z. B. benutzerdefinierte Tabs in Chrome. Unter Webinhalte in Ihrer Android-App verwenden erfahren Sie, wie Sie den richtigen Ansatz für Ihre Anwendungsfälle (z. B. Surfen oder Authentifizierung) auswählen.

Zusätzliche Ressourcen