قرار دادن یک وب ویو در Compose

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

یک وب ویو را با اندروید ویو پوشش دهید

برای استفاده از یک 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)			
            }
        }
    )
}

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

وضعیت 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 به عقب حرکت کند.

از API مربوط به Compose BackHandler برای رهگیری رویداد back سیستم استفاده کنید و تابع goBack() WebView را فراخوانی کنید:

// ...
@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)
            }
        }
    )
}

برای اطلاعات بیشتر در مورد insets، به درک window insets در WebView مراجعه کنید.

همگام‌سازی تم برنامه با محتوای WebView

وقتی برنامه بین حالت روشن و تاریک تغییر حالت می‌دهد، اگر به درستی مدیریت شود، محتوای WebView می‌تواند بدون بارگذاری مجدد صفحه، به طور خودکار به‌روزرسانی شود.

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

مدیریت مجوزهای دوربین و میکروفون

وقتی یک صفحه وب درخواست دسترسی به دوربین یا میکروفون (مثلاً WebRTC یا ضبط ویدیو) را می‌دهد، WebView تابع WebChromeClient.onPermissionRequest را فراخوانی می‌کند.

با این حال، قبل از فراخوانی grant() ، باید مجوزهای زمان اجرای اندروید زیر را درخواست کنید:

  • 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 تعبیه شده

اگر ترجیح می‌دهید از تعبیه WebView خودداری کنید، اندروید گزینه‌های دیگری برای نمایش محتوای وب، مانند Chrome Custom Tabs، ارائه می‌دهد. برای درک نحوه انتخاب رویکرد صحیح برای موارد استفاده خود (مانند مرور یا احراز هویت)، به بخش «استفاده از محتوای وب در برنامه اندروید» مراجعه کنید.