如要在 Jetpack Compose 中使用 WebView,必須將其納入 AndroidView。本指南說明常見用途,以及如何在 Compose 中支援這些用途。
使用 AndroidView 包裝 WebView
如要在 Compose 中使用 WebView,請用 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) } } ) }
這適用於在應用程式中顯示簡單的網址。不過,WebView 會處理複雜的狀態生命週期,這些生命週期與 Android View 生命週期和 Compose 生命週期不同。整合 Compose 可能會導致複雜的 WebView 情境,進而產生難以解決的錯誤。以下各節說明可能需要特定處理方式才能支援這些功能的用途。
保留 WebView 狀態
在 Compose 中處理設定變更和導覽相當困難,因為 WebView 是繫結至其主機 Activity 的舊版 View,且不建議其執行個體存留時間超過 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 內向後導覽,而不是退出畫面。
使用 Compose BackHandler API 攔截系統返回事件,並呼叫 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 } ) } } }
這種實作方式可提供瀏覽器樣式的導覽行為。
巢狀捲動
在 Compose 中使用 WebView 時,不支援巢狀捲動功能。將 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.CAMERAManifest.permission.RECORD_AUDIO
首先,請為 WebView 定義權限處理常式,追蹤從 WebView 要求的 PermissionRequest:
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 應用程式中使用網路內容」,瞭解如何根據用途 (例如瀏覽或驗證) 選擇正確的方法。