使用 WebViewCompat.navigate 提升網頁瀏覽體驗

WebViewCompat.navigateWebView.loadUrl 的強化替代方案,可精細控管網頁載入、記錄管理,以及 WebView 中的導覽生命週期追蹤。

先前使用 loadUrl 啟動網頁導覽時,有下列顯著限制:

  • 無法取代記錄項目:您無法取代目前的記錄項目,因此必須將項目新增至返回堆疊,才能前往新網頁。
  • 回呼已解除耦合:WebViewClient 中,沒有直接機制可將特定 loadUrl 呼叫與後續回呼事件建立關聯。
  • 未儲存額外標頭:傳遞至 loadUrl 的自訂標頭未儲存為 WebView 狀態的一部分,因此還原狀態時會遺失。

WebViewCompat.navigate API 導入下列功能,解決這些問題:

  • 取代導覽記錄項目:可讓您取代 WebView 記錄堆疊中的目前頁面。
  • 相關回呼追蹤:傳回 Navigation 物件,做為導覽生命週期所有階段的專屬 ID。
  • 支援儲存狀態標頭:額外標頭會可靠地儲存在 WebView 狀態套件中,以便在還原狀態時重複使用。

主要功能和限制

採用 WebViewCompat.navigate 前,請先考量下列作業規則和限制:

  • 執行緒安全:您必須在 UI (主) 執行緒上叫用 WebViewCompat.navigate

  • 取消和優先順序:無法明確取消進行中的導覽。不過,在同一個 WebView 上啟動新的 navigate 呼叫,會取代任何有效的導覽。

  • 支援 URI 配置:支援標準 (例如 https:http:) 和自訂 URI 配置。系統不支援 javascript: 配置。

  • 網址大小限制:支援的網址字串長度上限為 2 MB。

  • 功能檢查:請務必先使用 WebViewFeature.isFeatureSupported 檢查功能是否可用,再叫用 API,確保不同 WebView APK 版本之間的相容性。

啟動導航並追蹤生命週期

如要設定導覽並追蹤其生命週期,請按照下列步驟操作:

  1. WebView 設定期間,使用 WebViewCompat.addNavigationListener 註冊 NavigationListener 實作項目,即可接收結構化生命週期回呼。註冊監聽器一次 (而非每次導覽呼叫時),避免發生記憶體洩漏和重複執行回呼的情況。
  2. 使用 NavigationParameters.Builder 建構 NavigationParameters 執行個體,指定選用行為,例如取代記錄或自訂 HTTP 標頭。
  3. 呼叫 WebViewCompat.navigate,並傳遞 WebView 執行個體、到達網頁網址和參數。

WebViewCompat.navigate 會傳回可單獨識別要求的 Navigation 物件。在 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。 常見原因包括:

  • 針對必要非空值參數 (webViewurlparams) 傳遞 null
  • 提供不支援的網址配置,例如 javascript:
  • 傳遞格式錯誤的 HTTP 標頭鍵或值,不符合 RFC 2616 規格。

如果網路要求或載入網頁期間發生失敗 (例如 HTTP 404 狀態碼、DNS 解析失敗或 SSL 錯誤),WebViewCompat.navigate 仍會傳回有效的 Navigation 物件。

導覽完成後,請檢查 onNavigationCompleted 回呼內 Navigation 執行個體上的下列方法,診斷失敗原因:

儲存狀態套件管理

使用 NavigationParameters 傳遞額外標頭時,WebView 會將這些標頭儲存在已儲存的狀態套件中,以便在還原狀態時重複使用。不過,如果標頭數量龐大,儲存狀態 Bundle 的大小可能會大幅增加。

如要限制套裝組合大小,避免在 Android 狀態儲存期間發生 TransactionTooLargeException,請使用 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。

  • 關聯導覽例項:管理多個 WebView 例項時,使用傳回的 Navigation 物件區分並行導覽或篩選回呼。

  • 在初始化期間註冊一次監聽器:由於 WebViewCompat.addNavigationListener 會新增監聽器,而不是取代現有監聽器,因此請在 WebView 設定期間註冊一次 NavigationListener,避免記憶體洩漏,以及在後續導覽中重複執行回呼。

  • 監控儲存狀態大小:傳遞大型標頭酬載時,請使用 WebViewCompat.saveState 並明確指定大小界線,避免儲存過多的狀態資料。

其他資源

如要進一步瞭解內嵌網頁功能和效能最佳化,請參閱下列指南: