پیچیدن «وب‌نما» در Compose

برای استفاده از WebView در Jetpack Compose، باید آن را در AndroidView بپیچید. این راهنما موارد استفاده رایج و نحوه پشتیبانی از آن‌ها را در Compose توضیح می‌دهد.

پیچیدن «وب‌نما» با 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)			
            }
        }
    )
}

این روش برای نشان دادن نشانی وب ساده در برنامه شما کار می‌کند. بااین‌حال، WebView با چرخه‌های حیات وضعیت پیچیده سروکار دارد که از چرخه حیات «نمای Android» و چرخه حیات Compose جدا هستند. ادغام Compose می‌تواند سناریوهای پیچیده WebView ایجاد کند که منجر به اشکالات دشوار می‌شود. بخش‌های زیر موارد استفاده‌ای را شرح می‌دهند که ممکن است برای پشتیبانی از این ویژگی‌ها به رسیدگی خاصی نیاز داشته باشند.

حفظ وضعیت «وب‌نما»

مدیریت تغییرات پیکربندی و پیمایش در Compose چالش‌برانگیز است زیرا WebView یک View قدیمی است که به میزبان Activity خود محدود شده است، و توصیه نمی‌شود که نمونه آن بیشتر از چرخه حیات 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 در Compose، پیمایش تودرتو به‌راحتی پشتیبانی نمی‌شود. وقتی 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 می‌تواند به‌طور خودکار بدون بار کردن مجدد صفحه به‌روز شود، البته اگر به‌درستی مدیریت شود.

اگر مالک محتوای صفحه وب هستید، برای همگام‌سازی رنگ‌ها با زمینه برنامه، پرس‌وجوی رسانه 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.CAMERA
  • Manifest.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 }
    }
}

اجازه را از فراخوانی 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 اجتناب کنید، Android گزینه‌های دیگری برای نمایش محتوای وب ارائه می‌دهد، مانند برگه‌های سفارشی Chrome. برای درک نحوه انتخاب رویکرد صحیح برای موارد استفاده‌تان (مانند مرور یا اصالت‌سنجی)، استفاده از محتوای وب در برنامه Android را ببینید.

منابع بیشتر