تضمين 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 لفهم كيفية اختيار النهج الصحيح لحالات الاستخدام (مثل التصفّح أو المصادقة).

مراجع إضافية