لاستخدام 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.
WebView.
@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.CAMERAManifest.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 لمعرفة كيفية اختيار الطريقة الصحيحة
لحالات الاستخدام (مثل التصفّح أو المصادقة).