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