หากต้องการใช้ 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 แทนที่จะออกจากหน้าจอ
ใช้ 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 } ) } } }
การติดตั้งใช้งานนี้จะให้ลักษณะการนำทางแบบเบราว์เซอร์
การเลื่อนที่ฝังไว้
ระบบไม่รองรับการเลื่อนที่ซ้อนกันได้ง่ายๆ เมื่อใช้ 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 จะทริกเกอร์ Callback ที่เฉพาะเจาะจงใน WebChromeClient คุณต้องจัดการ Callback เหล่านี้และตรวจสอบว่ามีการให้สิทธิ์รันไทม์ของ Android ที่เกี่ยวข้อง
จัดการสิทธิ์เข้าถึงกล้องและไมโครโฟน
เมื่อหน้าเว็บขอสิทธิ์เข้าถึงกล้องหรือไมโครโฟน (เช่น WebRTC หรือการบันทึกวิดีโอ) WebView จะเรียกใช้ WebChromeClient.onPermissionRequest
อย่างไรก็ตาม ก่อนเรียกใช้ grant() คุณต้องขอสิทธิ์รันไทม์ของ Android ต่อไปนี้
Manifest.permission.CAMERAManifest.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 } }
ถัดไป ให้สร้าง Composable ที่จดจำ 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 } } }
จัดการสิทธิ์จาก Callback 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 เพื่อทำความเข้าใจวิธีเลือกแนวทางที่ถูกต้องสำหรับกรณีการใช้งานของคุณ (เช่น การเรียกดูหรือการตรวจสอบสิทธิ์)
แหล่งข้อมูลเพิ่มเติม
- จัดการและวิเคราะห์หน่วยความจำ WebView
- จัดการสถานะ WebView อย่างมีประสิทธิภาพ
- แก้ไขข้อบกพร่องของเว็บแอป