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) } } ) }
Hal ini berfungsi untuk menampilkan URL sederhana dalam aplikasi Anda. Namun, WebView menangani
siklus proses status kompleks yang terpisah dari siklus proses View Android
dan siklus proses Compose. Mengintegrasikan Compose dapat menimbulkan skenario
WebView yang kompleks dan menyebabkan 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 merupakan tantangan karena
WebView adalah View lama yang terikat ke Activity host-nya, dan
tidak direkomendasikan 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 akan mengarahkan
mundur dalam WebView, bukan keluar dari layar.
Gunakan Compose BackHandler API untuk menangkap 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 bergaya browser.
Scrolling bertingkat
Scrolling 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, menyusunnya bertingkat dengan
LazyColumn saat ini tidak berfungsi dengan baik.
Untuk melacak 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 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.
Agar elemen native seperti dropdown dan pop-up dapat 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 bukan pemilik konten web, penggelapan algoritme 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-nya. 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 memilih untuk tidak menyematkan WebView, Android menyediakan opsi lain untuk
menampilkan konten web, seperti Tab Khusus Chrome. Lihat Menggunakan konten web
dalam aplikasi Android 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