כדי להשתמש ב-WebView ב-Jetpack Compose, צריך להוסיף אותו ל-AndroidView.
במדריך הזה מוסבר על תרחישי שימוש נפוצים ואיך לתמוך בהם ב-Compose.
עטיפת WebView באמצעות AndroidView
כדי להשתמש ב-WebView בכתיבת אימייל, מוסיפים אותו בין התגים 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 API כדי ליירט את אירוע החזרה של המערכת, וקוראים לפונקציה 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 בכתיבה, אין תמיכה קלה בגלילה מקוננת. כשמציבים רכיב WebView בתוך מאגר של רכיב Compose שאפשר לגלול בו, כמו WebView, יכול להיות שרכיב WebView יתפוס את כל תנועות הגלילה.LazyColumn
מכיוון שהרכיב 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) } } ) }
אם לדף האינטרנט אין עיצוב כהה, או אם התוכן לא בבעלותכם, יכול להיות שהכהייה אלגוריתמית תעזור לכם להחיל עיצוב כהה. אתרים מודרניים שכבר כוללים מצב כהה מתעלמים מהאלגוריתם הזה ומשתמשים בסגנונות המובנים שלהם במקום זאת.
איך מטפלים בהרשאות לאתרים בכלי הכתיבה
כשדף אינטרנט מבקש גישה לחומרה או לנתונים (לדוגמה, מצלמה, מיקרופון או מיקום), WebView מפעיל קריאות חוזרות ספציפיות ב-WebChromeClient. צריך לטפל בקריאות החוזרות האלה ולוודא שההרשאות המתאימות של סביבת זמן הריצה ב-Android ניתנות.
ניהול הרשאות גישה למצלמה ולמיקרופון
כשדף אינטרנט מבקש גישה למצלמה או למיקרופון (למשל, WebRTC או הקלטת וידאו), מתבצעת קריאה ל-WebViewWebChromeClient.onPermissionRequest.
עם זאת, לפני שמתקשרים אל grant(), צריך לבקש את ההרשאות הבאות של Android בזמן הריצה:
Manifest.permission.CAMERAManifest.permission.RECORD_AUDIO
קודם מגדירים handler להרשאות עבור 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 } } }
מטפלים בהרשאה מהקריאה החוזרת (callback) של 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 מוסבר איך לבחור את הגישה הנכונה לתרחישים לדוגמה (כמו עיון או אימות).