تضمين WebView في Compose

لاستخدام WebView في Jetpack Compose، يجب تضمينه في AndroidView. يشرح هذا الدليل حالات الاستخدام الشائعة وكيفية دعمها في Compose.

تضمين WebView باستخدام 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)			
            }
        }
    )
}

تنجح هذه الطريقة في عرض عنوان 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 بدلاً من الخروج من الشاشة.

استخدِم واجهة برمجة التطبيقات BackHandler في Compose لاعتراض حدث الرجوع في النظام، و استدعِ الدالة 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

عندما ينتقل التطبيق بين الوضعَين الفاتح والداكن، يمكن تعديل محتوى 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
    }
}

بعد ذلك، أنشِئ عنصرًا قابلاً للإنشاء يتذكّر 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، يوفّر Android خيارات أخرى لـ عرض محتوى الويب، مثل علامات التبويب المخصَّصة في Chrome. اطّلِع على مقالة استخدام محتوى الويب داخل تطبيق Android لمعرفة كيفية اختيار الطريقة الصحيحة لحالات الاستخدام (مثل التصفّح أو المصادقة).