ניווט משופר בדף באמצעות WebViewCompat.navigate

WebViewCompat.navigate הוא חלופה משופרת ל-WebView.loadUrl שמאפשרת שליטה מדויקת בטעינת דפי אינטרנט, בניהול ההיסטוריה ובמעקב אחר מחזור החיים של הניווט ב-WebView.

בעבר, לניווטים בדפים באמצעות loadUrl היו מגבלות משמעותיות:

  • אי אפשר להחליף רשומה בהיסטוריה: אי אפשר להחליף את הרשומה הנוכחית בהיסטוריה, ולכן אי אפשר לנווט לדף חדש בלי להוסיף רשומה למחסנית הדפים הקודמים.
  • קודים להתקשרות חזרה שאינם תלויים זה בזה: לא היה מנגנון ישיר שיכול לקשר בין שיחה ספציפית של loadUrl לבין אירועים של קודים להתקשרות חזרה שמתרחשים לאחר מכן ב-WebViewClient.
  • כותרות נוספות לא נשמרו: כותרות מותאמות אישית שהועברו אל loadUrl לא נשמרו כחלק מהמצב של WebView, ולכן הן אבדו כששוחזר המצב.

WebViewCompat.navigate API פותר את הבעיות האלה באמצעות התכונות הבאות:

  • החלפת רשומה בהיסטוריית הניווט: מאפשרת להחליף את הדף הנוכחי במחסנית ההיסטוריה WebView.
  • מעקב אחר קריאות חוזרות עם קורלציה: מחזיר אובייקט Navigation שמשמש כמזהה ייחודי בכל השלבים של מחזור החיים של הניווט.
  • תמיכה בכותרות של מצב שמור: כותרות נוספות נשמרות באופן מהימן בחבילת המצב WebView, כך שאפשר לעשות בהן שימוש חוזר כשמשחזרים את המצב.

יכולות ומגבלות עיקריות

לפני שמאמצים את WebViewCompat.navigate, כדאי להביא בחשבון את כללי ההפעלה והמגבלות הבאים:

  • בטיחות שרשור: צריך להפעיל את WebViewCompat.navigate בשרשור ה-UI (הראשי).

  • ביטול ועדיפות: אי אפשר לבטל באופן מפורש ניווטים בתהליך. עם זאת, התחלת שיחה חדשה ב-navigate באותו WebView מבטלת כל ניווט פעיל.

  • תמיכה בסכימת URI: יש תמיכה בסכימות URI סטנדרטיות (כמו https: ו-http:) ובסכימות URI בהתאמה אישית. אין תמיכה בסכימה javascript:.

  • מגבלת הגודל של כתובת URL: האורך המקסימלי של מחרוזת כתובת URL שנתמך הוא 2MB.

  • בדיקת תכונות: כדי לשמור על תאימות בין גרסאות שונות של WebView APK, תמיד כדאי לבדוק את הזמינות של התכונות באמצעות WebViewFeature.isFeatureSupported לפני שמפעילים את ה-API.

התחלת ניווט ומעקב אחר מחזור החיים

כדי להגדיר ניווט ולעקוב אחרי מחזור החיים שלו:

  1. כדי לקבל קריאות חוזרות מובנות של מחזור החיים, צריך לרשום הטמעה של NavigationListener באמצעות WebViewCompat.addNavigationListener במהלך ההגדרה של WebView. כדי למנוע דליפות זיכרון והפעלות כפולות של קריאות חוזרות (callback), צריך לרשום את מאזין האירועים פעם אחת (ולא בכל קריאה לניווט).
  2. יוצרים מופע של NavigationParameters באמצעות NavigationParameters.Builder כדי לציין התנהגויות אופציונליות, כמו החלפה של היסטוריה או כותרות HTTP בהתאמה אישית.
  3. מתקשרים אל WebViewCompat.navigate ומעבירים את המופע WebView, את כתובת היעד ואת הפרמטרים.

WebViewCompat.navigate מחזירה אובייקט Navigation שמזהה באופן ייחודי את הבקשה. בפונקציות הקריאה החוזרות (callback) של NavigationListener, משווים את האובייקט הזה לפרמטר Navigation הנכנס כדי לעקוב אחרי הניווט הספציפי הזה.

דוגמה להטמעה

בדוגמה הבאה מוסבר איך להגדיר פרמטרים של ניווט, להפעיל את WebViewCompat.navigate ולהאזין לאירועים של מחזור החיים של הניווט:

Kotlin

class WebNavigationManager(private val webView: WebView) {
    // Track the navigation instance returned by the API
    private var currentNavigation: Navigation? = null

    init {
        // 1. Define listener to observe navigation lifecycle events
        val listener = object : NavigationListener {
            override fun onNavigationStarted(navigation: Navigation) {
                if (navigation == currentNavigation) {
                    // Navigation started
                }
            }

            override fun onNavigationRedirected(navigation: Navigation) {
                if (navigation == currentNavigation) {
                    // Navigation encountered a redirect
                }
            }

            override fun onNavigationCompleted(navigation: Navigation) {
                if (navigation == currentNavigation) {
                    if (navigation.didCommit()) {
                        // Navigation committed successfully
                    } else if (navigation.didCommitErrorPage()) {
                        // Navigation committed an error page
                        val statusCode = navigation.statusCode
                        val error = navigation.webResourceError
                    }
                }
            }

            override fun onFirstContentfulPaintMillis(page: Page, durationMillis: Long) {
                // Match page with current navigation
                if (page == currentNavigation?.page) {
                    // Page rendering started (First Contentful Paint achieved)
                }
            }
        }

        // 2. Register listener on the main thread
        WebViewCompat.addNavigationListener(webView, listener)
    }

    @UiThread
    fun navigateToPage(url: String) {
        // Check feature availability
        if (!WebViewFeature.isFeatureSupported(WebViewFeature.WEBVIEW_NAVIGATE_EXPERIMENTAL_V1)) {
            // Fall back to standard loadUrl if navigate API is unavailable
            webView.loadUrl(url)
            return
        }

        // 3. Configure navigation parameters
        val params = NavigationParameters.Builder()
            .setShouldReplaceCurrentEntry(true)
            .addAdditionalHeaders(
                mapOf("X-Test-Navigate-Header" to "TestValue")
            )
            .build()

        // 4. Initiate navigation on the UI thread
        currentNavigation = WebViewCompat.navigate(webView, url, params)
    }
}

Java

public class WebNavigationManager {

    private Navigation mCurrentNavigation;
    private final WebView mWebView;

    public WebNavigationManager(@NonNull WebView webView) {
        mWebView = webView;
        setupListener();
    }

    private void setupListener() {
        // 1. Define listener to observe navigation lifecycle events
        NavigationListener listener = new NavigationListener() {
            @Override
            public void onNavigationStarted(@NonNull Navigation navigation) {
                if (navigation.equals(mCurrentNavigation)) {
                    // Navigation started
                }
            }

            @Override
            public void onNavigationRedirected(@NonNull Navigation navigation) {
                if (navigation.equals(mCurrentNavigation)) {
                    // Navigation encountered a redirect
                }
            }

            @Override
            public void onNavigationCompleted(@NonNull Navigation navigation) {
                if (navigation.equals(mCurrentNavigation)) {
                    if (navigation.didCommit()) {
                        // Navigation committed successfully
                    } else if (navigation.didCommitErrorPage()) {
                        // Navigation committed an error page
                        int statusCode = navigation.getStatusCode();
                        WebResourceErrorCompat error = navigation.getWebResourceError();
                    }
                }
            }

            @Override
            public void onFirstContentfulPaintMillis(@NonNull Page page, long durationMillis) {
                if (mCurrentNavigation != null && page.equals(mCurrentNavigation.getPage())) {
                    // Page rendering started (First Contentful Paint achieved)
                }
            }
        };

        // 2. Register listener on the main thread
        WebViewCompat.addNavigationListener(mWebView, listener);
    }

    @UiThread
    public void navigateToPage(@NonNull String url) {
        // Check feature availability
        if (!WebViewFeature.isFeatureSupported(WebViewFeature.WEBVIEW_NAVIGATE_EXPERIMENTAL_V1)) {
            // Fall back to standard loadUrl if navigate API is unavailable
            mWebView.loadUrl(url);
            return;
        }

        // 3. Configure navigation parameters
        NavigationParameters params = new NavigationParameters.Builder()
            .setShouldReplaceCurrentEntry(true)
            .addAdditionalHeaders(Collections.singletonMap(
                "X-Test-Navigate-Header", "TestValue"
            ))
            .build();

        // 4. Initiate navigation on the UI thread
        mCurrentNavigation = WebViewCompat.navigate(mWebView, url, params);
    }
}

מצבי כשל וטיפול בשגיאות

WebViewCompat.navigate API מספק מנגנונים נפרדים לטיפול בשגיאות בהגדרות ובכשלים בניווט בזמן ריצה:

חריגים של ארגומנטים לא תקינים

העברת ארגומנטים לא תקינים מפעילה IllegalArgumentException סינכרוני. הסיבות הנפוצות לכך הן:

  • העברת הערך null לפרמטרים נדרשים שלא יכולים להיות null (‏webView, ‏url או params).
  • הזנת סכמת URL שלא נתמכת, כמו javascript:.
  • העברת מפתחות או ערכים של כותרות HTTP שאינם בפורמט תקין או שלא עומדים בדרישות של מפרט RFC 2616.

אם מתרחשת שגיאה במהלך בקשה לאחזור מהרשת או טעינת הדף (למשל קוד סטטוס HTTP 404, שגיאת פענוח DNS או שגיאת SSL), הפונקציה WebViewCompat.navigate עדיין מחזירה אובייקט Navigation תקין.

בסיום הניווט, בודקים את השיטות הבאות בקריאה החוזרת (callback) של Navigationהמופע בתוך onNavigationCompleted כדי לאבחן את הכשל:

  • getStatusCode: מחזירה את קוד הסטטוס של תגובת ה-HTTP (לדוגמה, 404 או 500).
  • getWebResourceError: מחזירה אובייקט WebResourceErrorCompat עם פרטים על שגיאות ברשת, כמו פסק זמן לחיבור או כשל בחיפוש מארח.
  • didCommitErrorPage: מציין אם WebView ביצע פעולה והציג למשתמש דף שגיאה.
  • didCommit: מציין אם הניווט בוצע בהצלחה לדף היעד בלי שהופסק.

שמירת ניהול חבילת מצב

כשמעבירים כותרות נוספות עם NavigationParameters,‏ WebView שומר את הכותרות האלה בחבילת המצב השמור שלו, כדי שאפשר יהיה להשתמש בהן מחדש כשמשחזרים את המצב. עם זאת, אוספים גדולים של כותרות יכולים להגדיל באופן משמעותי את הגודל של המצב השמור Bundle.

אם אתם צריכים להגביל את גודל החבילה כדי למנוע TransactionTooLargeException במהלך שמירת מצב ב-Android, אתם יכולים להשתמש ב-WebViewCompat.saveState. השיטה הזו מאפשרת להגדיר מגבלה על הגודל המקסימלי של החבילה בבייטים, ואפשרות להחריג פריטים מהיסטוריית ההעברה:

Kotlin

// Save state with a maximum bundle size limit (for example, 64 KB)
val maxSizeBytes = 64 * 1024
val includeForwardState = false
val outState = Bundle()

WebViewCompat.saveState(webView, outState, maxSizeBytes, includeForwardState)

Java

// Save state with a maximum bundle size limit (for example, 64 KB)
int maxSizeBytes = 64 * 1024;
boolean includeForwardState = false;
Bundle outState = new Bundle();

WebViewCompat.saveState(webView, outState, maxSizeBytes, includeForwardState);

החבילה שמתקבלת תואמת לשיטה הרגילה WebView.restoreState.

המלצות להעברה ולהטמעה

כדי להבטיח ביצועים ויציבות אופטימליים כשמנווטים ב-WebView, מומלץ לפעול לפי ההמלצות הבאות:

  • העברה מ-loadUrl אל navigate: העברה של כל השיחות בגרסה הקודמת של WebView.loadUrl אל WebViewCompat.navigate. כך אפשר לוודא שניהול ההיסטוריה אחיד ושהכותרות נשמרות תמיד כחלק מהמצב השמור.

  • תמיד בודקים את התמיכה בתכונה: לפני שמפעילים את ה-API, צריך לוודא שיש תמיכה בזמן ריצה באמצעות WebViewFeature.isFeatureSupported כדי להגן על עצמכם מפני גרסאות ישנות יותר של WebView.

  • התאמה בין מופעים של ניווט: אפשר להשתמש באובייקט Navigation שמוחזר כדי להבחין בין ניווטים מקבילים או לסנן קריאות חוזרות (callback) כשמנהלים כמה מופעים של WebView.

  • רישום של listener פעם אחת במהלך האתחול: מכיוון ש-WebViewCompat.addNavigationListener מוסיף listener במקום להחליף listener קיים, צריך לרשום את NavigationListener פעם אחת במהלך ההגדרה של WebView כדי למנוע דליפות זיכרון והפעלות כפולות של callback בניווטים הבאים.

  • מעקב אחרי גודל מצב השמירה: כשמעבירים מטען ייעודי (payload) גדול של כותרות, כדאי להשתמש ב-WebViewCompat.saveState עם גבולות גודל מפורשים כדי להימנע משמירה של נתוני מצב עודפים.

מקורות מידע נוספים

מידע נוסף על יכולות מוטמעות של אתרים ועל אופטימיזציה של ביצועים מופיע במדריכים הבאים: