تضمين 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 بدلاً من الخروج من الشاشة.

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

إذا لم تتضمّن صفحة الويب مظهرًا داكنًا أو إذا لم تكن تملك محتوى الويب، قد تساعد ميزة التعتيم الخوارزمي في فرض مظهر داكن. تتجاهل المواقع الإلكترونية الحديثة التي تتضمّن ميزة &quot;الوضع الداكن&quot; هذه الخوارزمية وتستخدم بدلاً منها الأنماط المضمّنة الخاصة بها.

التعامل مع أذونات الويب في Compose

عندما تطلب صفحة ويب الوصول إلى أجهزة أو بيانات (مثل الكاميرا أو الميكروفون أو الموقع الجغرافي)، يؤدي ذلك إلى تشغيل عمليات ردّ نداء معيّنة في WebViewWebChromeClient. عليك التعامل مع عمليات معاودة الاتصال هذه والتأكّد من منح أذونات وقت التشغيل المقابلة في نظام التشغيل 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 للتعرّف على كيفية اختيار الطريقة الصحيحة لحالات الاستخدام (مثل التصفّح أو المصادقة).

مراجع إضافية