Aby użyć WebView w Jetpack Compose, musisz umieścić go w AndroidView.
Z tego przewodnika dowiesz się, jakie są typowe przypadki użycia i jak je obsługiwać w Compose.
Zawijanie WebView za pomocą AndroidView
Aby użyć WebView w Compose, umieść go w 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) } } ) }
To rozwiązanie sprawdza się w przypadku wyświetlania w aplikacji prostego adresu URL. Jednak WebView zajmuje się złożonymi cyklami życia stanu, które są oddzielone od cyklu życia widoku Androida i cyklu życia Compose. Integracja Compose może wprowadzać złożone
WebView scenariusze, które powodują trudne do wykrycia błędy. W kolejnych sekcjach opisujemy przypadki użycia, które mogą wymagać specjalnego traktowania, aby obsługiwać te funkcje.
Zachowywanie stanu WebView
Obsługa zmian konfiguracji i nawigacji w Compose jest trudna, ponieważ WebView to starszy View powiązany z hostem Activity, a nie zaleca się, aby jego instancja przetrwała cykl życia Activity.
Dlatego standardowym sposobem utrwalania stanu elementu WebView jest umożliwienie niszczenia i ponownego tworzenia instancji WebView wraz z elementem Activity. Możesz ręcznie utrwalać wewnętrzną historię nawigacji i stan przewijania za pomocą 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() ) }
Obsługa przechodzenia wstecz
Jeśli WebView ma historię nawigacji, gest cofania w systemie powinien powodować cofnięcie w ramach WebView, a nie wyjście z ekranu.
Użyj interfejsu Compose BackHandler API, aby przechwycić zdarzenie systemowego przycisku Wstecz i wywołać funkcję 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 } ) } } }
Ta implementacja zapewnia nawigację w stylu przeglądarki.
Przewijanie zagnieżdżone
Zagnieżdżone przewijanie nie jest łatwo obsługiwane, gdy w Compose używasz elementu WebView. Jeśli umieścisz element WebView w kontenerze Compose z możliwością przewijania, np. w LazyColumn, WebView może on przechwytywać wszystkie gesty przewijania.
Ponieważ element WebView korzysta z własnego wewnętrznego silnika renderowania, zagnieżdżanie go w elemencie LazyColumn nie działa obecnie prawidłowo.
Aby śledzić postępy w zakresie oficjalnej obsługi zagnieżdżonego przewijania w WebView, zapoznaj się z tym problemem.
Układy od krawędzi do krawędzi i wstawki okien
W przypadku układów od krawędzi do krawędzi WebViewtreści mogą pojawiać się pod paskami systemowymi, takimi jak pasek stanu. Możesz użyć modyfikatora windowInsetsPadding, aby przesunąć cały element WebView do bezpiecznego obszaru:
@Composable fun EdgeToEdgeDemo(url: String) { AndroidView( modifier = Modifier .fillMaxSize() .windowInsetsPadding(WindowInsets.systemBars), factory = { context -> WebView(context).apply { loadUrl(url) } } ) }
Więcej informacji o wcięciach znajdziesz w artykule Wcięcia w oknie WebView.
Synchronizowanie motywu aplikacji z treściami WebView
Gdy aplikacja przełącza się między trybem jasnym a ciemnym, WebViewtreści mogą aktualizować się automatycznie bez ponownego wczytywania strony, jeśli są prawidłowo obsługiwane.
Jeśli jesteś właścicielem treści na stronie internetowej, aby zsynchronizować kolory z motywem aplikacji, obsłuż zapytanie o media prefers-color-scheme, aby mieć pewność, że strona internetowa dostosuje się do wybranego motywu.
Aby umożliwić elementom natywnym, takim jak menu i wyskakujące okienka, wykrywanie i dopasowywanie motywu aplikacji, zastosuj DayNight motyw stylu do 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) } } ) }
Jeśli strona internetowa nie ma trybu ciemnego lub nie jesteś właścicielem treści internetowych, algorytmiczne przyciemnianie może pomóc w wymuszeniu trybu ciemnego. Nowoczesne witryny, które mają już tryb ciemny, ignorują ten algorytm i zamiast niego używają własnych wbudowanych stylów.
Obsługa uprawnień sieciowych w Compose
Gdy strona internetowa prosi o dostęp do sprzętu lub danych (np. kamery, mikrofonu lub lokalizacji), WebViewwywołuje w swoim WebChromeClientokreślone wywołania zwrotne. Musisz obsługiwać te wywołania zwrotne i zapewnić przyznanie odpowiednich uprawnień środowiska wykonawczego Androida.
Zarządzanie uprawnieniami do kamery i mikrofonu
Gdy strona internetowa poprosi o dostęp do kamery lub mikrofonu (np. WebRTC lub nagrywanie wideo), WebView wywoła WebChromeClient.onPermissionRequest.
Zanim jednak wywołasz funkcję grant(), musisz poprosić o te uprawnienia Androida na czas działania:
Manifest.permission.CAMERAManifest.permission.RECORD_AUDIO
Najpierw zdefiniuj moduł obsługi uprawnień dla WebView, który śledzi PermissionRequest żądane z 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 } }
Następnie utwórz funkcję kompozycyjną, która zapamiętuje wartość WebViewPermissionHandler. Użyj
rememberLauncherForActivityResult, aby poprosić o uprawnienia:
@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 } } }
Obsłuż uprawnienia z wywołania zwrotnego onPermissionRequest. Spowoduje to uruchomienie narzędzia do zarządzania uprawnieniami:
@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() ) }
Alternatywa dla umieszczonego komponentu WebView
Jeśli wolisz unikać osadzania WebView, Android udostępnia inne opcje wyświetlania treści internetowych, takie jak karty niestandardowe Chrome. Więcej informacji o tym, jak wybrać odpowiednie podejście do swoich przypadków użycia (np. przeglądania lub uwierzytelniania), znajdziesz w artykule Korzystanie z treści internetowych w aplikacji na Androida.
Dodatkowe materiały
- Zarządzanie pamięcią komponentu WebView i diagnozowanie jej
- Efektywne zarządzanie stanem komponentu WebView
- Debugowanie aplikacji internetowych