Navegación de páginas mejorada con WebViewCompat.navigate

WebViewCompat.navigate es una alternativa mejorada a WebView.loadUrl que proporciona un control detallado sobre la carga de páginas web, la administración del historial y el seguimiento del ciclo de vida de la navegación en WebView.

Anteriormente, iniciar la navegación de páginas con loadUrl tenía limitaciones notables:

  • No se reemplazó la entrada del historial: No pudiste reemplazar la entrada del historial actual, por lo que no se puede navegar a una página nueva sin agregar una entrada a la pila de historial.
  • Devoluciones de llamada desacopladas: No había ningún mecanismo directo para correlacionar una llamada loadUrl específica con eventos de devolución de llamada posteriores en WebViewClient.
  • No se guardaron encabezados adicionales: Los encabezados personalizados que se pasaron a loadUrl no se guardaron como parte del estado de WebView, por lo que se perdieron cuando se restableció el estado.

La API de WebViewCompat.navigate resuelve estos problemas con las siguientes funciones:

  • Reemplazo de entrada del historial de navegación: Te permite reemplazar la página actual en el historial de la pila de WebView.
  • Seguimiento de devoluciones de llamada correlacionadas: Devuelve un objeto Navigation que sirve como identificador único en todas las etapas de un ciclo de vida de navegación.
  • Compatibilidad con encabezados de estado guardado: Los encabezados adicionales se guardan de forma confiable en el paquete de estado WebView para que se puedan reutilizar cuando se restablezca el estado.

Funciones y limitaciones clave

Antes de adoptar WebViewCompat.navigate, considera las siguientes reglas y restricciones operativas:

  • Seguridad de subprocesos: Debes invocar WebViewCompat.navigate en el subproceso de IU (principal).

  • Cancelación y prioridad: Las navegaciones en curso no se pueden cancelar de forma explícita. Sin embargo, iniciar una nueva llamada de navigate en el mismo WebView reemplaza cualquier navegación activa.

  • Compatibilidad con esquemas de URI: Se admiten esquemas de URI estándar (como https: y http:) y personalizados. No se admite el esquema javascript:.

  • Límite de tamaño de la URL: La longitud máxima admitida de la cadena de URL es de 2 MB.

  • Verificación de funciones: Siempre verifica la disponibilidad de las funciones con WebViewFeature.isFeatureSupported antes de invocar la API para mantener la compatibilidad en diferentes versiones del APK de WebView.

Inicia la navegación y haz un seguimiento del ciclo de vida

Para configurar la navegación y hacer un seguimiento de su ciclo de vida, haz lo siguiente:

  1. Registra una implementación de NavigationListener con WebViewCompat.addNavigationListener durante la configuración de WebView para recibir devoluciones de llamada estructuradas del ciclo de vida. Registra el objeto de escucha una vez (en lugar de en cada llamada de navegación) para evitar pérdidas de memoria y ejecuciones duplicadas de devoluciones de llamada.
  2. Construye una instancia de NavigationParameters con NavigationParameters.Builder para especificar comportamientos opcionales, como el reemplazo del historial o encabezados HTTP personalizados.
  3. Llama a WebViewCompat.navigate y pasa tu instancia de WebView, la URL de destino y los parámetros.

WebViewCompat.navigate devuelve un objeto Navigation que identifica la solicitud de forma única. En tus devoluciones de llamada de NavigationListener, compara este objeto con el parámetro Navigation entrante para hacer un seguimiento de esa navegación específica.

Ejemplo de implementación

En el siguiente ejemplo, se muestra cómo configurar parámetros de navegación, invocar WebViewCompat.navigate y escuchar eventos del ciclo de vida de la navegación:

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);
    }
}

Propaga el estado de la app con encabezados HTTP

Las apps web suelen requerir contexto de la app para Android host para coordinar la lógica de backend o personalizar el contenido web. Agregar parámetros de consulta a la URL para pasar esta información puede sobrecargar las URLs, interferir con el almacenamiento en caché y exponer el estado interno de la app.

En cambio, te recomendamos que pases el contexto de la app con encabezados HTTP personalizados. Si usas WebViewCompat.navigate y NavigationParameters, puedes enviar estos datos de forma segura a tu servidor. Además, WebView conserva estos encabezados durante la restauración del estado, lo que garantiza que el contenido web siga siendo coherente en los cambios de configuración. Ten en cuenta que esta persistencia solo se aplica cuando se usa WebViewCompat.navigate. Si usas WebView.loadUrl, los encabezados personalizados no se guardan en el paquete de estado WebView y se pierden cuando se restablecen.

Casos de uso habituales

Estos son algunos casos de uso comunes para pasar el contexto de la app host:

  • Versión de la app (X-App-Version): Pasar la versión de lanzamiento de la app host (como BuildConfig.VERSION_NAME) ayuda a tu servidor de backend a verificar la compatibilidad del puente de JavaScript nativo, controlar el acceso a las funciones o solicitar a los usuarios que actualicen apps más antiguas.
  • Plataforma del cliente (X-Client-Platform): Identificar de forma explícita el entorno host como Android permite que el servidor entregue vínculos a la tienda de rutas o IU adaptadas a la plataforma sin depender del análisis de la cadena User-Agent.

Ejemplo de implementación

En el siguiente ejemplo, se muestra cómo pasar la versión de la aplicación y la plataforma del cliente a un servidor 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);

Modos de falla y manejo de errores

La API de WebViewCompat.navigate proporciona mecanismos distintos para controlar los errores de configuración y las fallas de navegación en el tiempo de ejecución:

Excepciones de argumentos no válidos

Si se pasan argumentos no válidos, se activa un IllegalArgumentException síncrono. Entre las causas comunes, se incluyen las siguientes:

  • Se pasa null para los parámetros obligatorios no nulos (webView, url o params).
  • Se proporcionó un esquema de URL no admitido, como javascript:.
  • Se pasan claves o valores de encabezado HTTP con formato incorrecto que no cumplen con las especificaciones de RFC 2616.

Si se produce una falla durante la solicitud de red o la carga de la página (como un código de estado HTTP 404, una falla en la resolución de DNS o un error de SSL), WebViewCompat.navigate aún devuelve un objeto Navigation válido.

Cuando finalice la navegación, inspecciona los siguientes métodos en la instancia de Navigation dentro de la devolución de llamada de onNavigationCompleted para diagnosticar la falla:

  • getStatusCode: Devuelve el código de estado de la respuesta HTTP (por ejemplo, 404 o 500).
  • getWebResourceError: Devuelve un objeto WebResourceErrorCompat que detalla los errores de red, como los tiempos de espera de conexión o las fallas en la búsqueda de host.
  • didCommitErrorPage: Indica si WebView confirmó y mostró una página de error al usuario.
  • didCommit: Indica si la navegación se confirmó correctamente en una página de destino sin que se anulara.

Administración de paquetes de estado guardado

Cuando pasas encabezados adicionales con NavigationParameters, WebView guarda estos encabezados en su paquete de estado guardado para que se puedan reutilizar cuando se restablezca el estado. Sin embargo, las colecciones grandes de encabezados pueden aumentar considerablemente el tamaño del estado guardado Bundle.

Si necesitas restringir el tamaño del paquete para evitar TransactionTooLargeException durante los guardados de estado de Android, usa WebViewCompat.saveState. Este método te permite establecer un límite de tamaño máximo del paquete en bytes y, de manera opcional, excluir los elementos del historial de avance:

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);

El paquete resultante sigue siendo compatible con el método WebView.restoreState estándar.

Recomendaciones de migración e implementación

Para garantizar un rendimiento y una estabilidad óptimos cuando navegues en WebView, sigue estas recomendaciones:

  • Migra de loadUrl a navigate: Migra todas las llamadas heredadas de WebView.loadUrl a WebViewCompat.navigate. Esto garantiza una administración uniforme del historial y que los encabezados siempre se guarden como parte del estado guardado.

  • Siempre verifica la compatibilidad con la función: Antes de invocar la API, confirma la compatibilidad en el tiempo de ejecución con WebViewFeature.isFeatureSupported para protegerte contra versiones anteriores de WebView.

  • Correlaciona instancias de navegación: Usa el objeto Navigation que se devolvió para diferenciar las navegaciones simultáneas o filtrar las devoluciones de llamada cuando administres varias instancias de WebView.

  • Registra el objeto de escucha una vez durante la inicialización: Dado que WebViewCompat.addNavigationListener agrega un objeto de escucha en lugar de reemplazar uno existente, registra tu NavigationListener una vez durante la configuración de WebView para evitar pérdidas de memoria y ejecuciones duplicadas de devoluciones de llamada en navegaciones posteriores.

  • Supervisa el tamaño del estado de guardado: Cuando pases cargas útiles de encabezado grandes, usa WebViewCompat.saveState con límites de tamaño explícitos para evitar guardar datos de estado excesivos.

Recursos adicionales

Para obtener más información sobre las capacidades web integradas y la optimización del rendimiento, consulta las siguientes guías: