Membungkus WebView di Compose

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.CAMERA
  • Manifest.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).