כדי להשתמש ב-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 במקום לצאת מהמסך.
משתמשים ב-API של 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 יופיע מתחת לסרגלי מידע כמו שורת הסטטוס. אפשר להשתמש ב-modifier 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 מפעיל קריאות חוזרות (callbacks) ספציפיות ב-WebChromeClient שלו. אתם צריכים לטפל בקריאות החוזרות האלה ולוודא שניתנו הרשאות מתאימות בתחילת ההפעלה של Android.
איך מטפלים בהרשאות גישה למצלמה ולמיקרופון
כשדף אינטרנט מבקש גישה למצלמה או למיקרופון (לדוגמה, WebRTC או הקלטת וידאו), WebView קורא ל-WebChromeClient.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 מוסבר איך לבחור את הגישה הנכונה לתרחישים לדוגמה (כמו גלישה או אימות).