หากต้องการใช้ WebView ใน Jetpack Compose คุณต้องห่อด้วย AndroidView
คู่มือนี้อธิบาย Use Case ที่พบบ่อยและวิธีรองรับ Use Case เหล่านี้ใน 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 และไม่แนะนำให้ใช้อินสแตนซ์ของ 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 } ) } } }
การใช้งานนี้จะให้ลักษณะการทำงานของการนำทางแบบเบราว์เซอร์
การเลื่อนที่ฝังไว้
ระบบไม่รองรับการเลื่อนที่ซ้อนกันเมื่อใช้ 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เนื้อหาจะ
อัปเดตโดยอัตโนมัติได้โดยไม่ต้องโหลดหน้าเว็บซ้ำหากจัดการอย่างถูกต้อง
หากคุณเป็นเจ้าของเนื้อหาหน้าเว็บ ให้ซิงค์สีกับธีมของแอปโดย
จัดการ Media Query 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 ที่ติดตาม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 เพื่อทําความเข้าใจวิธีเลือกแนวทางที่ถูกต้อง
สําหรับกรณีการใช้งานของคุณ (เช่น การเรียกดูหรือการตรวจสอบสิทธิ์)