使用 WebViewCompat.navigate 增强了网页导航功能

WebViewCompat.navigateWebView.loadUrl 的增强型替代方案,可对 WebView 中的网页加载、历史记录管理和导航生命周期跟踪进行精细控制。

之前,使用 loadUrl 启动网页导航存在明显的限制:

  • 无法替换历史记录条目:您无法替换当前历史记录条目,因此无法在不向后退堆栈添加条目的情况下导航到新网页。
  • 解耦的回调:在 WebViewClient 中,没有直接机制将特定的 loadUrl 调用与后续回调事件相关联。
  • 未保存额外的标头:传递给 loadUrl 的自定义标头未保存为 WebView 状态的一部分,因此在恢复状态时会丢失。

WebViewCompat.navigate API 通过引入以下功能来解决这些问题:

  • 导航历史记录条目替换:用于替换 WebView 历史记录堆栈中的当前网页。
  • 相关回调跟踪:返回一个 Navigation 对象,该对象可作为导航生命周期所有阶段的唯一标识符。
  • 已保存状态标头支持:额外的标头会可靠地保存在 WebView 状态 bundle 中,以便在恢复状态时可以重复使用。

主要功能和限制

在采用 WebViewCompat.navigate 之前,请考虑以下操作规则和限制:

  • 线程安全:您必须在界面(主)线程上调用 WebViewCompat.navigate

  • 取消和优先级:无法明确取消飞行中的导航。不过,在同一 WebView 上发起新的 navigate 调用会取代任何有效的导航。

  • URI 协议支持:支持标准(例如 https:http:)和自定义 URI 协议。不支持 javascript: 方案。

  • 网址大小限制:支持的网址字符串长度上限为 2 MB。

  • 功能检查:在调用 API 之前,请务必使用 WebViewFeature.isFeatureSupported 检查功能是否可用,以保持不同 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。常见原因包括:

  • 为必需的非 null 参数(webViewurlparams)传递 null
  • 提供不受支持的网址协议,例如 javascript:
  • 传递格式错误且不符合 RFC 2616 规范的 HTTP 标头键或值。

如果在网络请求或网页加载期间发生故障(例如 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。

  • 关联导航实例:使用返回的 Navigation 对象来区分并发导航或在管理多个 WebView 实例时过滤回调。

  • 在初始化期间注册一次监听器:由于 WebViewCompat.addNavigationListener 会添加监听器,而不是替换现有监听器,因此请在 WebView 设置期间注册一次 NavigationListener,以避免内存泄漏和在后续导航中重复执行回调。

  • 监控保存状态大小:传递大型标头载荷时,请使用具有明确大小边界的 WebViewCompat.saveState,以避免保存过多的状态数据。

其他资源

如需详细了解嵌入式 Web 功能和性能优化,请参阅以下指南: