Để sử dụng WebView trong Jetpack Compose, bạn phải gói thành phần này trong một 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.
Bao bọc WebView bằng AndroidView
Để sử dụng WebView trong Compose, hãy gói WebView bằng một 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 Android View và vòng đời Compose. Việc tích hợp Compose có thể gây ra các tình huống WebView phức tạp dẫn đến các lỗi khó phát hiện. Các phần sau đây mô tả những trường hợp sử dụng có thể cần được xử lý cụ thể để hỗ trợ các tính năng đó.
Duy trì trạng thái WebView
Việc xử lý các thay đổi về cấu hình và thao tác điều hướng trong Compose là một việc khó khăn vì WebView là một View cũ được liên kết với Activity lưu trữ và không nên để phiên bản của WebView tồn tại lâu hơn vòng đời Activity.
Do đó, cách tiêu chuẩn để duy trì trạng thái của WebView là cho phép các thực thể WebView bị huỷ và tạo lại 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 ngược 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.
Lồng tính năng cuộn trong lớp khác
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 một 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 công cụ 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 tính năng cuộn lồng 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, nội dung WebView 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ể 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 nội dung nghe nhì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à so khớp giao diện ứng dụng, hãy áp dụng giao diện kiểu DayNight cho 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) } } ) }
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ì tính năng làm tối bằng thuật toán có thể giúp bạn buộc trang web chuyển sang giao diện tối. Những trang web hiện đại đã có chế độ tối sẽ bỏ qua thuật toán này và sử dụng các kiểu tích hợp riêng.
Xử lý các quyền trên 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ụ: camera, 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 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 ghi hình), 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 thời gian 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 muốn tránh nhúng WebView, Android cung cấp các lựa chọn khác để hiển thị nội dung 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 để 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 xem hoặc xác thực).