Aby używać WebView w Jetpack Compose, musisz umieścić ją w AndroidView.
Z tego przewodnika dowiesz się, jak obsługiwać typowe przypadki użycia w Compose.
Umieszczanie WebView w AndroidView
Aby używać WebView w Compose, umieść ją 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) } } ) }
Działa to w przypadku wyświetlania prostego adresu URL w aplikacji. Jednak WebView ma złożone cykle życia stanu, które są oddzielne od cyklu życia widoku Androida i cyklu życia Compose. Integracja Compose może wprowadzić złożone scenariusze WebView, 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.
Utrzymywanie 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 WebView jest umożliwienie niszczenia i ponownego tworzenia instancji WebView wraz z 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
Gdy WebView ma historię nawigacji, gest cofania w systemie powinien powodować przejście wstecz w WebView, a nie wyjście z ekranu.
Aby przechwycić zdarzenie cofania w systemie, użyj interfejsu API Compose BackHandler i
wywołaj 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 zachowanie nawigacji w stylu przeglądarki.
Przewijanie zagnieżdżone
Przewijanie zagnieżdżone nie jest łatwo obsługiwane, gdy używasz WebView w Compose. Umieszczając WebView w przewijanym kontenerze Compose, np. LazyColumn, WebView może zużywać wszystkie gesty przewijania.
Ponieważ WebView korzysta z własnego wewnętrznego silnika renderowania, zagnieżdżanie go w LazyColumn obecnie nie działa prawidłowo.
Aby śledzić postępy w zakresie oficjalnej obsługi przewijania zagnieżdżonego w WebView, zapoznaj się z
tym zgłoszeniem.
Układy od krawędzi do krawędzi i wcięcia okna
W przypadku układów od krawędzi do krawędzi treści WebView mogą pojawiać się pod paskami systemowymi, takimi jak pasek stanu. Aby przesunąć całą WebView do bezpiecznego obszaru, możesz użyć modyfikatora windowInsetsPadding:
@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 okna w WebView.
Synchronizowanie motywu aplikacji z treścią WebView
Gdy aplikacja przełącza się między trybem jasnym a ciemnym, treści WebView mogą aktualizować się automatycznie bez ponownego wczytywania strony, jeśli są prawidłowo obsługiwane.
Jeśli jesteś właścicielem treści strony internetowej, aby zsynchronizować kolory z motywem aplikacji, obsłuż zapytanie o media prefers-color-scheme, aby mieć pewność, że strona internetowa dostosowuje się do wybranego motywu.
Aby umożliwić elementom natywnym, takim jak listy rozwijane i wyskakujące okienka, wykrywanie i dopasowywanie motywu aplikacji
, zastosuj motyw stylu DayNight 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 ciemnego motywu lub nie jesteś właścicielem treści internetowych, algorytmiczne przyciemnianie może pomóc w wymuszeniu ciemnego motywu. Nowoczesne witryny, które mają już tryb ciemny, ignorują ten algorytm i używają własnych wbudowanych stylów.
Obsługa uprawnień internetowych w Compose
Gdy strona internetowa prosi o dostęp do sprzętu lub danych (np. do kamery, mikrofonu lub lokalizacji), WebView wywołuje określone wywołania zwrotne w WebChromeClient. Musisz obsłużyć te wywołania zwrotne i upewnić się, że przyznano odpowiednie uprawnienia do działania w Androidzie.
Obsługa uprawnień do korzystania z kamery i mikrofonu
Gdy strona internetowa prosi o dostęp do kamery lub mikrofonu (np. WebRTC lub nagrywanie wideo), WebView wywołuje WebChromeClient.onPermissionRequest.
Zanim jednak wywołasz grant(), musisz poprosić o te uprawnienia do działania w Androidzie:
Manifest.permission.CAMERAManifest.permission.RECORD_AUDIO
Najpierw zdefiniuj obsługę uprawnień dla WebView, która śledzi PermissionRequest żądane przez 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 element kompozycyjny, który zapamiętuje WebViewPermissionHandler. Aby poprosić o uprawnienia, użyj rememberLauncherForActivityResult:
@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 obsługi uprawnień:
@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 wbudowanej WebView
Jeśli wolisz unikać osadzania WebView, Android udostępnia inne opcje wyświetlania treści internetowych, takie jak karty niestandardowe Chrome. Aby dowiedzieć się, jak wybrać odpowiednie podejście
do swoich przypadków użycia (np. przeglądania lub uwierzytelniania), przeczytaj artykuł Korzystanie z treści internetowych
w aplikacji na Androida.