Untuk menggunakan WebView di Jetpack Compose, Anda harus membungkusnya dalam AndroidView.
Panduan ini menjelaskan kasus penggunaan umum dan cara mendukungnya di Compose.
Membungkus 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 yang terpisah dari siklus proses Android View dan siklus proses Compose. Mengintegrasikan Compose dapat menimbulkan 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 cukup 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 secara manual mempertahankan histori navigasi internal dan status scroll-nya 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 akan menavigasi
mundur dalam WebView, bukan keluar dari layar.
Gunakan API BackHandler Compose 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.
Scrolling 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, menyarangkannya dengan
LazyColumn saat ini tidak berfungsi dengan baik.
Untuk memantau progres dukungan scroll bertingkat resmi untuk WebView, lihat masalah ini.
Tata letak layar penuh dan inset jendela
Saat menggunakan tata letak layar penuh, konten WebView dapat 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 tersebut, penggelapan algoritma dapat membantu menerapkan tema gelap. Situs modern yang sudah memiliki mode gelap akan mengabaikan algoritma ini dan menggunakan gaya bawaan mereka sendiri.
Menangani izin web di Compose
Saat halaman web meminta akses hardware atau data (misalnya, kamera,
mikrofon, atau lokasi), WebView 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 memanggil WebChromeClient.onPermissionRequest.
Namun, sebelum memanggil grant(), Anda harus meminta izin waktu proses 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 yang disematkan
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).
Referensi lainnya
- Mengelola dan mendiagnosis memori WebView
- Mengelola status WebView secara efisien
- Men-debug aplikasi web