WebViewCompat.navigate 是 WebView.loadUrl 的增强型替代方案,可在 WebView 中对网页加载、历史记录管理和导航生命周期跟踪进行精细控制。
之前,使用 loadUrl 启动网页导航存在明显的限制:
- 无法替换历史记录条目:您无法替换当前历史记录条目,因此无法在不向后退堆栈添加条目的情况下导航到新网页。
- 解耦的回调:在
WebViewClient中,没有直接机制将特定的loadUrl调用与后续回调事件相关联。 - 未保存额外的标头:传递给
loadUrl的自定义标头未保存为WebView状态的一部分,因此在恢复状态时会丢失。
WebViewCompat.navigate API 通过引入以下功能来解决这些问题:
- 导航历史记录条目替换:用于替换
WebView历史记录堆栈中的当前网页。 - 相关回调跟踪:返回一个
Navigation对象,该对象可作为导航生命周期所有阶段的唯一标识符。 - 支持保存状态标头:额外的标头会可靠地保存在
WebView状态软件包中,以便在恢复状态时可以重复使用。
主要功能和限制
在采用 WebViewCompat.navigate 之前,请考虑以下操作规则和限制:
线程安全:您必须在界面(主)线程上调用
WebViewCompat.navigate。取消和优先级:无法明确取消飞行中的导航。不过,在同一
WebView上发起新的navigate调用会取代任何有效的导航。URI 方案支持:支持标准(例如
https:和http:)和自定义 URI 方案。不支持javascript:方案。网址大小限制:支持的网址字符串长度上限为 2 MB。
功能检查:在调用 API 之前,请务必使用
WebViewFeature.isFeatureSupported检查功能是否可用,以保持不同 WebView APK 版本之间的兼容性。
启动导航并跟踪生命周期
如需配置导航并跟踪其生命周期,请执行以下操作:
- 在
WebView设置期间使用WebViewCompat.addNavigationListener注册NavigationListener实现,以接收结构化的生命周期回调。注册一次监听器(而不是在每次导航调用时注册),以防止内存泄漏和重复执行回调。 - 使用
NavigationParameters.Builder构建NavigationParameters实例,以指定可选行为,例如历史记录替换或自定义 HTTP 标头。 - 调用
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);
}
}
使用 HTTP 标头传播应用状态
Web 应用通常需要来自宿主 Android 应用的上下文来协调后端逻辑或自定义 Web 内容。将查询参数附加到网址以传递此信息可能会使网址杂乱无章,干扰缓存,并暴露内部应用状态。
我们建议改用自定义 HTTP 标头传递应用上下文。通过使用 WebViewCompat.navigate 和 NavigationParameters,您可以安全地将这些数据发送到服务器。此外,WebView 在状态恢复期间会保留这些标头,从而确保 Web 内容在配置更改期间保持一致。请注意,此持久性仅在使用 WebViewCompat.navigate 时适用。如果您使用 WebView.loadUrl,自定义标头不会保存在 WebView 状态 bundle 中,并且会在恢复时丢失。
常见应用场景
传递宿主应用上下文的常见用例包括:
- 应用版本 (
X-App-Version):传递宿主应用的发布版本(例如BuildConfig.VERSION_NAME)有助于后端服务器验证原生 JavaScript 桥接兼容性、控制功能或提示用户更新旧版应用。 - 客户端平台 (
X-Client-Platform):明确将宿主环境标识为 Android,可让服务器提供量身定制的界面或路由商店链接,而无需依赖User-Agent字符串解析。
实现示例
以下示例演示了如何将应用版本和客户端平台传递给 Web 服务器:
Kotlin
// Attach host app metadata so the server can verify compatibility and tailor content
val params = NavigationParameters.Builder()
.addAdditionalHeaders(
mapOf(
"X-App-Version" to BuildConfig.VERSION_NAME,
"X-Client-Platform" to "Android"
)
)
.build()
// Use navigate instead of loadUrl to retain custom headers across state restoration
WebViewCompat.navigate(webView, "https://www.example.com", params)
Java
// Attach host app metadata so the server can verify compatibility and tailor content
Map<String, String> headers = new HashMap<>();
headers.put("X-App-Version", BuildConfig.VERSION_NAME);
headers.put("X-Client-Platform", "Android");
NavigationParameters params = new NavigationParameters.Builder()
.addAdditionalHeaders(headers)
.build();
// Use navigate instead of loadUrl to retain custom headers across state restoration
WebViewCompat.navigate(webView, "https://www.example.com", params);
故障模式和错误处理
WebViewCompat.navigate API 提供了用于处理配置错误和运行时导航失败的不同机制:
参数无效异常
传递无效实参会触发同步 IllegalArgumentException。常见原因包括:
- 为必需的非 null 参数(
webView、url或params)传递null。 - 提供不受支持的网址协议,例如
javascript:。 - 传递不符合 RFC 2616 规范的格式错误的 HTTP 标头键或值。
导航流程错误
如果在网络请求或网页加载期间发生故障(例如 HTTP 404 状态代码、DNS 解析失败或 SSL 错误),WebViewCompat.navigate 仍会返回有效的 Navigation 对象。
导航完成后,检查 onNavigationCompleted 回调中的 Navigation 实例上的以下方法,以诊断失败原因:
getStatusCode:返回 HTTP 响应状态代码(例如404或500)。getWebResourceError:返回一个WebResourceErrorCompat对象,其中详细说明了网络错误,例如连接超时或主机查找失败。didCommitErrorPage:表示WebView是否已提交并向用户显示错误页面。didCommit:表示导航是否成功提交到目标网页,而未被中止。
保存状态软件包管理
当您使用 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 功能和性能优化,请参阅以下指南: