Navigazione nelle pagine migliorata con WebViewCompat.navigate

WebViewCompat.navigate è un'alternativa avanzata a WebView.loadUrl che fornisce un controllo granulare sul caricamento delle pagine web, sulla gestione della cronologia e sul monitoraggio del ciclo di vita della navigazione in WebView.

In precedenza, l'avvio della navigazione nelle pagine utilizzando loadUrl presentava limiti notevoli:

  • Nessuna sostituzione della voce della cronologia: non è stato possibile sostituire la voce della cronologia corrente, rendendo impossibile passare a una nuova pagina senza aggiungere una voce al back stack.
  • Callback disaccoppiati: non esisteva un meccanismo diretto per correlare una chiamata loadUrl specifica con i successivi eventi di callback in WebViewClient.
  • Intestazioni aggiuntive non salvate: le intestazioni personalizzate trasmesse a loadUrl non sono state salvate come parte dello stato WebView, quindi sono andate perse durante il ripristino dello stato.

L'API WebViewCompat.navigate risolve questi problemi introducendo le seguenti funzionalità:

  • Sostituzione della voce della cronologia di navigazione:consente di sostituire la pagina corrente nella cronologia WebView.
  • Monitoraggio dei callback correlati: restituisce un oggetto Navigation che funge da identificatore univoco in tutte le fasi del ciclo di vita di una navigazione.
  • Supporto delle intestazioni dello stato salvato: le intestazioni aggiuntive vengono salvate in modo affidabile nel bundle dello stato WebView in modo che possano essere riutilizzate al ripristino dello stato.

Funzionalità e limitazioni principali

Prima di adottare WebViewCompat.navigate, tieni presente le seguenti regole e limitazioni operative:

  • Thread safety: devi richiamare WebViewCompat.navigate sul thread dell'interfaccia utente (principale).

  • Annullamento e precedenza:le navigazioni in volo non possono essere annullate esplicitamente. Tuttavia, l'avvio di una nuova chiamata navigate sullo stesso WebView ha la precedenza su qualsiasi navigazione attiva.

  • Supporto dello schema URI:sono supportati schemi URI standard (come https: e http:) e personalizzati. Lo schema javascript: non è supportato.

  • Limite di dimensioni dell'URL:la lunghezza massima della stringa URL supportata è 2 MB.

  • Controllo delle funzionalità:controlla sempre la disponibilità delle funzionalità utilizzando WebViewFeature.isFeatureSupported prima di richiamare l'API per mantenere la compatibilità tra le diverse versioni dell'APK WebView.

Avviare la navigazione e monitorare il ciclo di vita

Per configurare la navigazione e monitorarne il ciclo di vita:

  1. Registra un'implementazione di NavigationListener utilizzando WebViewCompat.addNavigationListener durante la configurazione di WebView per ricevere callback del ciclo di vita strutturati. Registra il listener una sola volta (anziché a ogni chiamata di navigazione) per evitare perdite di memoria ed esecuzioni duplicate dei callback.
  2. Crea un'istanza NavigationParameters utilizzando NavigationParameters.Builder per specificare comportamenti facoltativi, ad esempio la sostituzione della cronologia o le intestazioni HTTP personalizzate.
  3. Chiama WebViewCompat.navigate, passando l'istanza WebView, l'URL di destinazione e i parametri.

WebViewCompat.navigate restituisce un oggetto Navigation che identifica in modo univoco la richiesta. Nei callback NavigationListener, confronta questo oggetto con il parametro Navigation in entrata per monitorare questa navigazione specifica.

Esempio di implementazione

L'esempio seguente mostra come configurare i parametri di navigazione, richiamare WebViewCompat.navigate e ascoltare gli eventi del ciclo di vita della navigazione:

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

Propagare lo stato dell'app utilizzando le intestazioni HTTP

Le app web spesso richiedono il contesto dell'app per Android host per coordinare la logica di backend o personalizzare i contenuti web. L'aggiunta di parametri di ricerca all'URL per trasmettere queste informazioni può ingombrare gli URL, interferire con la memorizzazione nella cache ed esporre lo stato interno dell'app.

Ti consigliamo invece di trasmettere il contesto dell'app utilizzando intestazioni HTTP personalizzate. Utilizzando WebViewCompat.navigate e NavigationParameters, puoi inviare in modo sicuro questi dati al tuo server. Inoltre, WebView conserva queste intestazioni durante il ripristino dello stato, il che garantisce che i contenuti web rimangano coerenti in seguito alle modifiche alla configurazione. Tieni presente che questa persistenza si applica solo quando utilizzi WebViewCompat.navigate. Se utilizzi WebView.loadUrl, le intestazioni personalizzate non vengono salvate nel bundle di stato WebView e vengono perse al ripristino.

Casi d'uso comuni

I casi d'uso comuni per il passaggio del contesto dell'app host includono:

  • Versione dell'app (X-App-Version): il passaggio della versione di rilascio dell'app host (ad esempio BuildConfig.VERSION_NAME) aiuta il server di backend a verificare la compatibilità del bridge JavaScript nativo, a controllare le funzionalità o a chiedere agli utenti di aggiornare le app meno recenti.
  • Piattaforma client (X-Client-Platform): l'identificazione esplicita dell'ambiente host come Android consente al server di fornire un'interfaccia utente personalizzata per la piattaforma o link allo store di route senza fare affidamento all'analisi della stringa User-Agent.

Esempio di implementazione

L'esempio seguente mostra come trasmettere la versione dell'applicazione e la piattaforma client a un server 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);

Modalità di errore e gestione degli errori

L'API WebViewCompat.navigate fornisce meccanismi distinti per la gestione degli errori di configurazione e degli errori di navigazione di runtime:

Eccezioni di argomenti non validi

Il passaggio di argomenti non validi attiva un IllegalArgumentException sincrono. Le cause comuni includono:

  • Passaggio di null per i parametri obbligatori non nulli (webView, url o params).
  • Fornire uno schema URL non supportato, ad esempio javascript:.
  • Trasmissione di chiavi o valori di intestazione HTTP non validi che non sono conformi alle specifiche RFC 2616.

Se si verifica un errore durante la richiesta di rete o il caricamento pagina (ad esempio un codice di stato HTTP 404, un errore di risoluzione DNS o un errore SSL), WebViewCompat.navigate restituisce comunque un oggetto Navigation valido.

Al termine della navigazione, esamina i seguenti metodi nell'istanza Navigation all'interno del callback onNavigationCompleted per diagnosticare l'errore:

  • getStatusCode: restituisce il codice di stato della risposta HTTP (ad esempio, 404 o 500).
  • getWebResourceError: restituisce un oggetto WebResourceErrorCompat che descrive in dettaglio gli errori di rete, come timeout di connessione o errori di ricerca dell'host.
  • didCommitErrorPage: indica se WebView ha eseguito il commit e mostrato una pagina di errore all'utente.
  • didCommit: indica se la navigazione è stata eseguita correttamente in una pagina di destinazione senza essere interrotta.

Gestione dei bundle di stati salvati

Quando passi intestazioni aggiuntive con NavigationParameters, WebView salva queste intestazioni nel bundle di stati salvati in modo che possano essere riutilizzate al ripristino dello stato. Tuttavia, le raccolte di intestazioni di grandi dimensioni possono aumentare notevolmente le dimensioni dello stato salvato Bundle.

Se devi limitare le dimensioni del bundle per evitare TransactionTooLargeException durante i salvataggi dello stato di Android, utilizza WebViewCompat.saveState. Questo metodo ti consente di impostare un limite massimo di dimensioni del bundle in byte ed escludere facoltativamente gli elementi della cronologia avanti:

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

Il bundle risultante rimane compatibile con il metodo standard WebView.restoreState.

Consigli per la migrazione e l'implementazione

Per garantire prestazioni e stabilità ottimali durante la navigazione in WebView, segui questi consigli:

  • Eseguire la migrazione da loadUrl a navigate: esegui la migrazione di tutte le chiamate legacy WebView.loadUrl a WebViewCompat.navigate. In questo modo, la gestione della cronologia è uniforme e le intestazioni vengono sempre salvate come parte dello stato salvato.

  • Verifica sempre il supporto delle funzionalità:prima di richiamare l'API, conferma il supporto del runtime con WebViewFeature.isFeatureSupported per proteggerti dalle versioni precedenti di WebView.

  • Correlare le istanze di navigazione:utilizza l'oggetto Navigation restituito per differenziare le navigazioni simultanee o filtrare i callback durante la gestione di più istanze WebView.

  • Registra il listener una volta durante l'inizializzazione:poiché WebViewCompat.addNavigationListener aggiunge un listener anziché sostituirne uno esistente, registra NavigationListener una volta durante la configurazione di WebView per evitare perdite di memoria ed esecuzioni di callback duplicate nelle navigazioni successive.

  • Monitora le dimensioni dello stato di salvataggio:quando passi payload di intestazione di grandi dimensioni, utilizza WebViewCompat.saveState con limiti di dimensione espliciti per evitare di salvare dati di stato eccessivi.

Risorse aggiuntive

Per scoprire di più sulle funzionalità web incorporate e sull'ottimizzazione del rendimento, consulta le seguenti guide: