Untuk menggunakan WebView di Jetpack Compose, Anda harus menggabungkannya dalam AndroidView.
Panduan ini menjelaskan kasus penggunaan umum dan cara mendukungnya di Compose.
Menggabungkan WebView dengan AndroidView
Untuk menggunakan WebView di Compose, gabungkan dengan 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) } } ) }
Cara ini berfungsi untuk menampilkan URL sederhana dalam aplikasi Anda. Namun, WebView menangani siklus proses status yang kompleks dan terpisah dari siklus proses Android View dan siklus proses Compose. Mengintegrasikan Compose dapat memperkenalkan skenario WebView yang kompleks sehingga menghasilkan bug yang sulit. Bagian berikut menjelaskan kasus penggunaan yang mungkin memerlukan penanganan khusus untuk mendukung fitur tersebut.
Mempertahankan status WebView
Menangani perubahan konfigurasi dan navigasi di Compose sangat sulit karena
WebView adalah View lama yang terikat ke Activity host-nya, dan
tidak disarankan agar instance-nya bertahan lebih lama dari siklus proses Activity.
Oleh karena itu, cara standar untuk mempertahankan status WebView adalah dengan mengizinkan instance WebView dihancurkan dan dibuat ulang bersama dengan Activity. Anda dapat mempertahankan histori navigasi internal dan status scroll secara manual menggunakan 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() ) }
Menangani navigasi kembali
Jika WebView memiliki histori navigasi, gestur kembali sistem harus menavigasi mundur dalam WebView, bukan keluar dari layar.
Gunakan Compose BackHandler API untuk mencegat peristiwa kembali sistem, dan
panggil fungsi 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 } ) } } }
Implementasi ini memberikan perilaku navigasi gaya browser.
Scroll bertingkat
Scroll bertingkat tidak mudah didukung saat menggunakan WebView di Compose. Saat menempatkan WebView di dalam penampung Compose yang dapat di-scroll, seperti LazyColumn, WebView dapat menggunakan semua gestur scroll.
Karena WebView mengandalkan mesin rendering internalnya sendiri, menempatkannya dengan LazyColumn saat ini tidak berfungsi dengan baik.
Untuk melacak progres dukungan scroll bertingkat resmi untuk WebView, lihat
masalah ini.
Tata letak edge-to-edge dan inset jendela
Saat menggunakan tata letak edge-to-edge, konten WebView mungkin muncul di bawah kolom sistem seperti status bar. Anda dapat menggunakan pengubah windowInsetsPadding untuk mendorong seluruh WebView ke area aman:
@Composable fun EdgeToEdgeDemo(url: String) { AndroidView( modifier = Modifier .fillMaxSize() .windowInsetsPadding(WindowInsets.systemBars), factory = { context -> WebView(context).apply { loadUrl(url) } } ) }
Untuk mengetahui informasi selengkapnya tentang inset, lihat Memahami inset jendela di WebView.
Menyinkronkan tema aplikasi dengan konten WebView
Saat aplikasi beralih antara mode terang dan gelap, konten WebView dapat diperbarui secara otomatis tanpa memuat ulang halaman jika ditangani dengan benar.
Jika Anda memiliki konten halaman web, untuk menyinkronkan warna dengan tema aplikasi, tangani kueri media prefers-color-scheme untuk memastikan halaman web Anda beradaptasi dengan tema yang dipilih.
Untuk mengaktifkan elemen native seperti dropdown dan pop-up agar mendeteksi dan mencocokkan tema aplikasi Anda, terapkan tema gaya DayNight ke Activity. Anda.
<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) } } ) }
Jika halaman web tidak memiliki tema gelap, atau jika Anda tidak memiliki konten web, penggelapan algoritmik dapat membantu menerapkan tema gelap. Situs modern yang sudah memiliki mode gelap akan mengabaikan algoritma ini dan menggunakan gaya bawaannya sendiri.
Menangani izin web di Compose
Saat halaman web meminta akses hardware atau data (misalnya, kamera, mikrofon, atau lokasi), WebView akan memicu callback tertentu di WebChromeClient. Anda harus menangani callback ini dan memastikan izin runtime Android yang sesuai diberikan.
Menangani izin kamera dan mikrofon
Saat halaman web meminta akses kamera atau mikrofon (misalnya, WebRTC atau perekaman video), WebView akan memanggil WebChromeClient.onPermissionRequest.
Namun, sebelum memanggil grant(), Anda harus meminta izin runtime Android berikut:
Manifest.permission.CAMERAManifest.permission.RECORD_AUDIO
Pertama, tentukan pengendali izin untuk WebView yang melacak PermissionRequest yang diminta dari 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 } }
Selanjutnya, buat composable yang mengingat WebViewPermissionHandler. Gunakan rememberLauncherForActivityResult untuk meminta izin:
@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 } } }
Tangani izin dari callback onPermissionRequest. Tindakan ini akan meluncurkan peluncur izin:
@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() ) }
Alternatif untuk WebView tersemat
Jika Anda lebih suka menghindari penyematan WebView, Android menyediakan opsi lain untuk
menampilkan konten web, seperti Tab Khusus Chrome. Lihat Menggunakan konten web
dalam aplikasi Android Anda untuk memahami cara memilih pendekatan yang tepat
untuk kasus penggunaan Anda (seperti penjelajahan atau autentikasi).