Verbesserte Seitennavigation mit WebViewCompat.navigate

WebViewCompat.navigate ist eine erweiterte Alternative zu WebView.loadUrl, mit der Sie das Laden von Webseiten, die Verlaufsverwaltung und das Tracking des Navigationslebenszyklus in WebView detailliert steuern können.

Bisher gab es bei der Initiierung von Seitennavigationen mit loadUrl erhebliche Einschränkungen:

  • Kein Ersetzen von Verlaufseinträgen:Der aktuelle Verlaufseintrag konnte nicht ersetzt werden. Daher war es nicht möglich, zu einer neuen Seite zu wechseln, ohne dem Backstack einen Eintrag hinzuzufügen.
  • Entkoppelte Callbacks:Es gab keinen direkten Mechanismus, um einen bestimmten loadUrl-Aufruf mit nachfolgenden Callback-Ereignissen in WebViewClient zu korrelieren.
  • Zusätzliche Header nicht gespeichert:Benutzerdefinierte Header, die an loadUrl übergeben wurden, wurden nicht als Teil des WebView-Status gespeichert. Daher gingen sie beim Wiederherstellen des Status verloren.

Die WebViewCompat.navigate API behebt diese Probleme durch die Einführung der folgenden Funktionen:

  • Eintrag im Navigationsverlauf ersetzen:Damit können Sie die aktuelle Seite im WebView-Verlauf ersetzen.
  • Korrelierte Callback-Analyse:Gibt ein Navigation-Objekt zurück, das als eindeutige Kennzeichnung für alle Phasen eines Navigationslebenszyklus dient.
  • Unterstützung von Headern für gespeicherten Zustand:Zusätzliche Header werden zuverlässig im WebView-Zustands-Bundle gespeichert, damit sie beim Wiederherstellen des Zustands wiederverwendet werden können.

Wichtige Funktionen und Einschränkungen

Beachten Sie vor der Einführung von WebViewCompat.navigate die folgenden Betriebsregeln und Einschränkungen:

  • Threadsicherheit:Sie müssen WebViewCompat.navigate im UI-Thread (Hauptthread) aufrufen.

  • Abbrechen und Vorrang:Laufende Navigationen können nicht explizit abgebrochen werden. Wenn Sie jedoch einen neuen navigate-Anruf auf demselben WebView starten, wird die aktive Navigation überschrieben.

  • Unterstützung von URI-Schemas:Es werden Standard-URI-Schemas (z. B. https: und http:) und benutzerdefinierte URI-Schemas unterstützt. Das Schema javascript: wird nicht unterstützt.

  • URL-Größenbeschränkung:Die maximal unterstützte Länge des URL-Strings beträgt 2 MB.

  • Funktionsprüfung:Prüfen Sie immer die Verfügbarkeit von Funktionen mit WebViewFeature.isFeatureSupported, bevor Sie die API aufrufen, um die Kompatibilität mit verschiedenen WebView-APK-Versionen zu gewährleisten.

Navigation starten und Track-Lebenszyklus verfolgen

So konfigurieren Sie die Navigation und verfolgen ihren Lebenszyklus:

  1. Registrieren Sie eine NavigationListener-Implementierung mit WebViewCompat.addNavigationListener während der WebView-Einrichtung, um strukturierte Lebenszyklus-Callbacks zu erhalten. Registrieren Sie den Listener nur einmal (nicht bei jedem Navigationsaufruf), um Speicherlecks und doppelte Callback-Ausführungen zu vermeiden.
  2. Erstellen Sie eine NavigationParameters-Instanz mit NavigationParameters.Builder, um optionales Verhalten wie das Ersetzen des Verlaufs oder benutzerdefinierte HTTP-Header anzugeben.
  3. Rufen Sie WebViewCompat.navigate auf und übergeben Sie Ihre WebView-Instanz, die Ziel-URL und die Parameter.

WebViewCompat.navigate gibt ein Navigation-Objekt zurück, das die Anfrage eindeutig identifiziert. Vergleichen Sie dieses Objekt in Ihren NavigationListener-Callbacks mit dem eingehenden Navigation-Parameter, um diese bestimmte Navigation zu erfassen.

Implementierungsbeispiel

Das folgende Beispiel zeigt, wie Navigationsparameter konfiguriert, WebViewCompat.navigate aufgerufen und auf Navigationslebenszyklusereignisse gewartet wird:

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

App-Status mit HTTP-Headern weitergeben

Web-Apps benötigen oft Kontext von der Host-Android-App, um die Backend-Logik zu koordinieren oder Webinhalte anzupassen. Wenn Sie Suchparameter an die URL anhängen, um diese Informationen zu übergeben, kann das zu unübersichtlichen URLs führen, das Caching beeinträchtigen und den internen App-Status offenlegen.

Wir empfehlen stattdessen, den App-Kontext über benutzerdefinierte HTTP-Header zu übergeben. Mit WebViewCompat.navigate und NavigationParameters können Sie diese Daten sicher an Ihren Server senden. Außerdem werden diese Header von WebView beim Wiederherstellen des Status beibehalten. So wird dafür gesorgt, dass die Webinhalte bei Konfigurationsänderungen konsistent bleiben. Diese Persistenz gilt nur, wenn WebViewCompat.navigate verwendet wird. Wenn Sie WebView.loadUrl verwenden, werden benutzerdefinierte Header nicht im WebView-Status-Bundle gespeichert und gehen bei der Wiederherstellung verloren.

Gängige Anwendungsfälle

Häufige Anwendungsfälle für das Übergeben des Host-App-Kontexts:

  • App-Version (X-App-Version): Wenn Sie die Release-Version der Host-App (z. B. BuildConfig.VERSION_NAME) übergeben, kann Ihr Backend-Server die Kompatibilität der nativen JavaScript-Bridge überprüfen, Funktionen einschränken oder Nutzer auffordern, ältere Apps zu aktualisieren.
  • Clientplattform (X-Client-Platform): Wenn die Hostumgebung explizit als Android angegeben wird, kann der Server plattformspezifische Benutzeroberflächen bereitstellen oder Store-Links weiterleiten, ohne dass der User-Agent-String geparst werden muss.

Implementierungsbeispiel

Das folgende Beispiel zeigt, wie die Anwendungsversion und die Clientplattform an einen Webserver übergeben werden:

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

Fehlermodi und Fehlerbehandlung

Die WebViewCompat.navigate API bietet unterschiedliche Mechanismen für die Verarbeitung von Konfigurationsfehlern und Laufzeitnavigationsfehlern:

Ausnahmen für ungültige Argumente

Wenn ungültige Argumente übergeben werden, wird ein synchrones IllegalArgumentException ausgelöst. Häufige Ursachen sind:

  • null für erforderliche Parameter übergeben, die nicht null sein dürfen (webView, url oder params).
  • Sie geben ein nicht unterstütztes URL-Schema an, z. B. javascript:.
  • Übergeben von fehlerhaften HTTP-Headerschlüsseln oder -werten, die nicht den Spezifikationen von RFC 2616 entsprechen.

Wenn während der Netzwerkanfrage oder des Seitenaufrufs ein Fehler auftritt (z. B. ein HTTP-Statuscode 404, ein DNS-Auflösungsfehler oder ein SSL-Fehler), gibt WebViewCompat.navigate weiterhin ein gültiges Navigation-Objekt zurück.

Wenn die Navigation abgeschlossen ist, untersuchen Sie die folgenden Methoden für die Navigation-Instanz in Ihrem onNavigationCompleted-Callback, um den Fehler zu diagnostizieren:

  • getStatusCode: Gibt den HTTP-Antwortstatuscode zurück (z. B. 404 oder 500).
  • getWebResourceError: Gibt ein WebResourceErrorCompat-Objekt mit Details zu Netzwerkfehlern zurück, z. B. Verbindungszeitüberschreitungen oder Fehler bei der Hostsuche.
  • didCommitErrorPage: Gibt an, ob WebView übernommen und dem Nutzer eine Fehlerseite angezeigt wurde.
  • didCommit: Gibt an, ob die Navigation erfolgreich auf einer Zielseite ausgeführt wurde, ohne abgebrochen zu werden.

Verwaltung von Status-Bundles speichern

Wenn Sie zusätzliche Header mit NavigationParameters übergeben, speichert WebView diese Header im gespeicherten Status-Bundle, damit sie beim Wiederherstellen des Status wiederverwendet werden können. Große Sammlungen von Headern können jedoch die Größe des gespeicherten Status Bundle erheblich erhöhen.

Wenn Sie die Bundle-Größe einschränken müssen, um TransactionTooLargeException beim Speichern des Android-Status zu verhindern, verwenden Sie WebViewCompat.saveState. Mit dieser Methode können Sie ein maximales Limit für die Bundle-Größe in Byte festlegen und optional Elemente aus dem Vorwärtsverlauf ausschließen:

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

Das resultierende Bundle ist weiterhin mit der Standardmethode WebView.restoreState kompatibel.

Empfehlungen für die Migration und Implementierung

Damit Sie in WebView eine optimale Leistung und Stabilität erzielen, sollten Sie die folgenden Empfehlungen beachten:

  • Von loadUrl zu navigate migrieren:Migrieren Sie alle Legacy-WebView.loadUrl-Aufrufe zu WebViewCompat.navigate. So wird eine einheitliche Verlaufsverwaltung gewährleistet und dafür gesorgt, dass Kopfzeilen immer als Teil des gespeicherten Status gespeichert werden.

  • Funktionsunterstützung immer prüfen:Bevor Sie die API aufrufen, prüfen Sie mit WebViewFeature.isFeatureSupported, ob die Laufzeitumgebung unterstützt wird, um Probleme mit älteren WebView-Versionen zu vermeiden.

  • Navigationsinstanzen korrelieren:Mit dem zurückgegebenen Navigation-Objekt können Sie gleichzeitige Navigationen unterscheiden oder Rückrufe filtern, wenn Sie mehrere WebView-Instanzen verwalten.

  • Listener einmal während der Initialisierung registrieren:Da mit WebViewCompat.addNavigationListener ein Listener hinzugefügt und nicht ein vorhandener ersetzt wird, sollten Sie NavigationListener einmal während der WebView-Einrichtung registrieren, um Speicherlecks und doppelte Callback-Ausführungen bei nachfolgenden Navigationsvorgängen zu vermeiden.

  • Größe des gespeicherten Status überwachen:Wenn Sie große Header-Nutzlasten übergeben, verwenden Sie WebViewCompat.saveState mit expliziten Größenbeschränkungen, um zu vermeiden, dass zu viele Statusdaten gespeichert werden.

Zusätzliche Ressourcen

Weitere Informationen zu eingebetteten Webfunktionen und zur Leistungsoptimierung finden Sie in den folgenden Leitfäden: