Để sử dụng WebView trong Jetpack Compose, bạn phải gói thành phần này trong AndroidView.
Hướng dẫn này giải thích các trường hợp sử dụng phổ biến và cách hỗ trợ các trường hợp đó trong Compose.
Gói WebView bằng AndroidView
Để sử dụng WebView trong Compose, hãy gói thành phần này bằng 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) } } ) }
Cách này phù hợp để hiển thị một URL đơn giản trong ứng dụng của bạn. Tuy nhiên, WebView xử lý các vòng đời trạng thái phức tạp tách biệt với vòng đời của Chế độ xem Android và vòng đời của Compose. Việc tích hợp Compose có thể gây ra các tình huống phức tạp
WebView dẫn đến lỗi khó xử lý. Các phần sau đây mô tả những trường hợp sử dụng có thể cần xử lý cụ thể để hỗ trợ các tính năng đó.
Giữ nguyên trạng thái WebView
Việc xử lý các thay đổi về cấu hình và điều hướng trong Compose là một thách thức vì WebView là một View cũ liên kết với Activity lưu trữ và không nên để thực thể của nó tồn tại lâu hơn vòng đời của Activity.
Do đó, cách tiêu chuẩn để duy trì trạng thái của WebView là cho phép huỷ và tạo lại các thực thể WebView cùng với Activity. Bạn có thể duy trì nhật ký điều hướng nội bộ và trạng thái cuộn theo cách thủ công bằng cách sử dụng 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() ) }
Xử lý thao tác điều hướng quay lại
Khi WebView có nhật ký điều hướng, cử chỉ quay lại của hệ thống sẽ điều hướng quay lại trong WebView thay vì thoát khỏi màn hình.
Sử dụng API BackHandler của Compose để chặn sự kiện quay lại của hệ thống và gọi hàm 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 } ) } } }
Cách triển khai này cung cấp hành vi điều hướng theo kiểu trình duyệt.
Cuộn lồng
Tính năng cuộn lồng nhau không được hỗ trợ dễ dàng khi sử dụng WebView trong Compose. Khi đặt WebView bên trong vùng chứa Compose có thể cuộn, chẳng hạn như LazyColumn, WebView có thể sử dụng tất cả cử chỉ cuộn.
Vì WebView dựa vào công cụ kết xuất nội bộ riêng, nên việc lồng ghép thành phần này với LazyColumn hiện không hoạt động đúng cách.
Để theo dõi tiến trình hỗ trợ chính thức cho tính năng cuộn lồng nhau cho WebView, hãy xem vấn đề này.
Bố cục tràn viền và phần lồng ghép cửa sổ
Khi sử dụng bố cục tràn viền, WebView nội dung có thể xuất hiện bên dưới các thanh hệ thống, chẳng hạn như thanh trạng thái. Bạn có thể sử dụng đối tượng sửa đổi windowInsetsPadding để đẩy toàn bộ WebView vào vùng an toàn:
@Composable fun EdgeToEdgeDemo(url: String) { AndroidView( modifier = Modifier .fillMaxSize() .windowInsetsPadding(WindowInsets.systemBars), factory = { context -> WebView(context).apply { loadUrl(url) } } ) }
Để biết thêm thông tin về phần lồng ghép, hãy xem bài viết Tìm hiểu về phần lồng ghép cửa sổ trong WebView.
Đồng bộ hoá giao diện ứng dụng với nội dung WebView
Khi ứng dụng chuyển đổi giữa chế độ sáng và tối, nội dung WebView có thể tự động cập nhật mà không cần tải lại trang nếu được xử lý đúng cách.
Nếu bạn sở hữu nội dung trang web, để đồng bộ hoá màu sắc với giao diện của ứng dụng, hãy xử lý truy vấn phương tiện prefers-color-scheme để đảm bảo trang web của bạn thích ứng với giao diện đã chọn.
Để cho phép các phần tử gốc như trình đơn thả xuống và cửa sổ bật lên phát hiện và khớp với giao diện ứng dụng, hãy áp dụng giao diện kiểu DayNight cho Activity. của bạn
<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) } } ) }
Nếu trang web không có giao diện tối hoặc nếu bạn không sở hữu nội dung trên web, thì thuật toán làm tối có thể giúp buộc áp dụng giao diện tối. Các trang web hiện đại đã có chế độ tối sẽ bỏ qua thuật toán này và sử dụng kiểu tích hợp sẵn của riêng chúng.
Xử lý quyền truy cập web trong Compose
Khi một trang web yêu cầu quyền truy cập vào phần cứng hoặc dữ liệu (ví dụ: máy ảnh, micrô hoặc vị trí), WebView sẽ kích hoạt các lệnh gọi lại cụ thể trong WebChromeClient. Bạn phải xử lý các lệnh gọi lại này và đảm bảo cấp các quyền tương ứng khi bắt đầu chạy Android.
Xử lý quyền truy cập vào camera và micrô
Khi một trang web yêu cầu quyền truy cập vào camera hoặc micrô (ví dụ: WebRTC hoặc quay video), WebView sẽ gọi WebChromeClient.onPermissionRequest.
Tuy nhiên, trước khi gọi grant(), bạn phải yêu cầu các quyền khi bắt đầu chạy sau đây của Android:
Manifest.permission.CAMERAManifest.permission.RECORD_AUDIO
Trước tiên, hãy xác định một trình xử lý quyền cho WebView để theo dõi PermissionRequest được yêu cầu từ 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 } }
Tiếp theo, hãy tạo một thành phần kết hợp ghi nhớ WebViewPermissionHandler. Sử dụng rememberLauncherForActivityResult để yêu cầu cấp quyền:
@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 } } }
Xử lý quyền từ lệnh gọi lại onPermissionRequest. Thao tác này sẽ khởi chạy trình chạy quyền:
@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() ) }
Giải pháp thay thế cho WebView được nhúng
Nếu bạn không muốn nhúng WebView, Android sẽ cung cấp các lựa chọn khác để hiển thị nội dung trên web, chẳng hạn như Thẻ tuỳ chỉnh của Chrome. Hãy xem bài viết Sử dụng nội dung trên web trong ứng dụng Android để tìm hiểu cách chọn phương pháp phù hợp cho các trường hợp sử dụng của bạn (chẳng hạn như duyệt web hoặc xác thực).
Tài nguyên khác
- Quản lý và chẩn đoán bộ nhớ WebView
- Quản lý trạng thái WebView một cách hiệu quả
- Gỡ lỗi ứng dụng web