Чтобы использовать WebView в Jetpack Compose, его необходимо обернуть в AndroidView . В этом руководстве описаны распространенные сценарии использования и способы их поддержки в Compose.
Оберните WebView в AndroidView
Чтобы использовать WebView в Compose, оберните его в 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) } } ) }
Это работает для отображения простого URL-адреса в вашем приложении. Однако WebView работает со сложными жизненными циклами состояний, которые отделены от жизненного цикла Android View и жизненного цикла Compose. Интеграция Compose может привести к сложным сценариям работы WebView , что может вызвать серьезные ошибки. В следующих разделах описаны варианты использования, которые могут потребовать специальной обработки для поддержки этих функций.
Сохранение состояния WebView
Обработка изменений конфигурации и навигация в Compose представляют собой сложную задачу, поскольку WebView — это устаревший View привязанный к своей Activity , и не рекомендуется , чтобы его экземпляр существовал дольше, чем Activity .
Таким образом, стандартный способ сохранения состояния WebView — это разрешение на уничтожение и повторное создание экземпляров WebView вместе с Activity . Вы можете вручную сохранить его внутреннюю историю навигации и состояние прокрутки, используя 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() ) }
Назад к навигации
Если WebView имеет историю навигации, системный жест "назад" должен обеспечивать возврат назад внутри WebView , а не за пределы экрана.
Используйте API Compose BackHandler для перехвата системного события "Назад" и вызовите функцию 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 } ) } } }
Данная реализация обеспечивает навигацию в стиле браузера.
Вложенная прокрутка
Вложенная прокрутка плохо поддерживается при использовании WebView в Compose. При размещении WebView внутри прокручиваемого контейнера Compose, например, LazyColumn , WebView может перехватывать все жесты прокрутки. Поскольку WebView использует собственный внутренний механизм рендеринга, его вложенность с LazyColumn в настоящее время работает некорректно.
Чтобы отслеживать прогресс в разработке официальной поддержки вложенной прокрутки для WebView , см. эту проблему .
Макеты от края до края и оконные вставки
При использовании макетов от края до края содержимое WebView может отображаться под системными панелями, такими как строка состояния. Вы можете использовать модификатор windowInsetsPadding , чтобы переместить весь WebView в безопасную область:
@Composable fun EdgeToEdgeDemo(url: String) { AndroidView( modifier = Modifier .fillMaxSize() .windowInsetsPadding(WindowInsets.systemBars), factory = { context -> WebView(context).apply { loadUrl(url) } } ) }
Для получения дополнительной информации об отступах см. раздел «Понимание отступов окна в WebView» .
Синхронизация темы приложения с содержимым WebView
При переключении приложения между светлым и темным режимами содержимое WebView может автоматически обновляться без перезагрузки страницы, если это правильно обработано.
Если вы являетесь владельцем контента веб-страницы, для синхронизации цветов с темой приложения используйте медиа-запрос prefers-color-scheme чтобы убедиться, что ваша веб-страница адаптируется к выбранной теме.
Чтобы нативные элементы, такие как выпадающие списки и всплывающие окна, могли распознавать и соответствовать теме вашего приложения, примените тему в стиле DayNight к вашей 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) } } ) }
Если веб-страница не имеет темной темы или если вы не являетесь владельцем веб-контента, алгоритмическое затемнение может помочь принудительно включить темную тему. Современные веб-сайты, которые уже имеют темный режим, игнорируют этот алгоритм и используют вместо него свои собственные встроенные стили.
Управление правами доступа к веб-ресурсам в Compose.
Когда веб-страница запрашивает доступ к оборудованию или данным (например, к камере, микрофону или местоположению), WebView запускает специальные обратные вызовы в своем WebChromeClient . Вам необходимо обработать эти обратные вызовы и убедиться, что предоставлены соответствующие разрешения среды выполнения Android.
Настройте права доступа к камере и микрофону.
Когда веб-страница запрашивает доступ к камере или микрофону (например, WebRTC или видеозапись), WebView вызывает WebChromeClient.onPermissionRequest .
Однако, прежде чем вызывать grant() , необходимо запросить следующие разрешения среды выполнения Android:
-
Manifest.permission.CAMERA -
Manifest.permission.RECORD_AUDIO
Во-первых, определите обработчик разрешений для WebView , который будет отслеживать запрос PermissionRequest , отправленный из 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 } }
Далее создайте компонент, который запоминает WebViewPermissionHandler . Используйте 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 } } }
Обработайте запрос на разрешение из функции обратного вызова onPermissionRequest . Это запустит средство запуска запросов на разрешение:
@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() ) }
Альтернатива встроенному WebView
Если вы предпочитаете избегать встраивания WebView , Android предоставляет другие варианты отображения веб-контента, например, пользовательские вкладки Chrome . См. раздел «Использование веб-контента в вашем приложении Android» , чтобы понять, как выбрать правильный подход для ваших сценариев использования (например, для просмотра веб-страниц или аутентификации).