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 navegación en WebView.

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

  • No se reemplaza la entrada del historial: No podías reemplazar la entrada del historial actual, lo que imposibilitaba navegar a una página nueva sin agregar una entrada a la pila de actividades.
  • Devoluciones de llamada desacopladas: No había un mecanismo directo para correlacionar una llamada loadUrl específica con eventos de devolución de llamada posteriores en WebViewClient.
  • No se guardan los encabezados adicionales: Los encabezados personalizados que se pasan a loadUrl no se guardan como parte del estado WebView, por lo que se perdían cuando se restablecía el estado.

La API de WebViewCompat.navigate resuelve estos problemas con la introducción de las siguientes funciones:

  • Reemplazo de entrada del historial de navegación: Te permite reemplazar la página actual en la pila del historial de WebView.
  • Seguimiento de devoluciones de llamada correlacionadas: Muestra un objeto Navigation que funciona como un 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.

Funcionalidades 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 precedencia: Las navegaciones en curso no se pueden cancelar de forma explícita. Sin embargo, iniciar una nueva llamada 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 URL: La longitud máxima de cadena de URL admitida es de 2 MB.

  • Verificación de funciones: Siempre verifica la disponibilidad de funciones con WebViewFeature.isFeatureSupported antes de invocar la API para mantener la compatibilidad en diferentes versiones de 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 NavigationListener implementación con WebViewCompat.addNavigationListener durante WebView configuración 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 de devolución de llamada duplicadas.
  2. Crea 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 muestra un objeto Navigation que identifica de forma única la solicitud. En tus NavigationListener devoluciones de llamada, 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 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);
    }
}

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 pasas argumentos no válidos, se activa una IllegalArgumentException síncrona. Las causas comunes incluyen las siguientes:

  • Pasar null para parámetros obligatorios no nulos (webView, url o params).
  • Proporcionar un esquema de URL no admitido, como javascript:.
  • Pasar 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 código de estado, una falla de resolución de DNS o un error de SSL), WebViewCompat.navigate aún muestra un objeto Navigation válido.

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

  • getStatusCode: Muestra el código de estado de respuesta HTTP (por ejemplo, 404 o 500).
  • getWebResourceError: Muestra un objeto WebResourceErrorCompat que detalla los errores de red, como los tiempos de espera de conexión o las fallas de 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 anule.

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 Bundle de estado guardado.

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 máximo de tamaño de paquete en bytes y, de manera opcional, excluir 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 estándar WebView.restoreState.

Recomendaciones de migración e implementación

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

  • Migra de loadUrl a navigate: Migra todas las llamadas heredadas 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 funciones: Antes de invocar la API, confirma la compatibilidad con el tiempo de ejecución con WebViewFeature.isFeatureSupported para protegerte de versiones anteriores de WebView.

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

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

  • Supervisa el tamaño del estado 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 incorporadas y la optimización del rendimiento, consulta las siguientes guías: