Ulepszona nawigacja na stronie za pomocą WebViewCompat.navigate

WebViewCompat.navigate to ulepszona alternatywa dla WebView.loadUrl, która zapewnia szczegółową kontrolę nad wczytywaniem stron internetowych, zarządzaniem historią i śledzeniem cyklu życia nawigacji w WebView.

Wcześniej inicjowanie nawigacji po stronach za pomocą loadUrl miało istotne ograniczenia:

  • Brak zastępowania wpisów w historii: nie można było zastąpić bieżącego wpisu w historii, co uniemożliwiało przejście do nowej strony bez dodawania wpisu do stosu wstecz.
  • Rozdzielone wywołania zwrotne: nie było bezpośredniego mechanizmu, który umożliwiałby powiązanie konkretnego wywołania loadUrl z kolejnymi zdarzeniami wywołania zwrotnego w WebViewClient.
  • Dodatkowe nagłówki nie są zapisywane: niestandardowe nagłówki przekazywane do loadUrl nie były zapisywane jako część stanu WebView, więc były tracone podczas przywracania stanu.

Interfejs API WebViewCompat.navigate rozwiązuje te problemy, wprowadzając te funkcje:

  • Zastępowanie wpisów w historii nawigacji: umożliwia zastąpienie bieżącej strony w stosie historii WebView.
  • Powiązane śledzenie wywołań zwrotnych: zwraca obiekt Navigation, który służy jako unikalny identyfikator na wszystkich etapach cyklu życia nawigacji.
  • Obsługa nagłówków zapisanych stanów: dodatkowe nagłówki są niezawodnie zapisywane w pakiecie stanu WebView, dzięki czemu można ich ponownie użyć po przywróceniu stanu.

Kluczowe możliwości i ograniczenia

Zanim zaczniesz korzystać z WebViewCompat.navigate, zapoznaj się z tymi regułami i ograniczeniami operacyjnymi:

  • Bezpieczeństwo wątków: musisz wywołać WebViewCompat.navigate w wątku interfejsu (głównym).

  • Anulowanie i pierwszeństwo: nawigacji w toku nie można wyraźnie anulować. Jednak zainicjowanie nowego wywołania navigate w tym samym WebView zastępuje każdą aktywną nawigację.

  • Obsługa schematów URI: obsługiwane są standardowe (np. https: i http:) oraz niestandardowe schematy URI. Schemat javascript: nie jest obsługiwany.

  • Limit rozmiaru adresu URL: maksymalna obsługiwana długość ciągu adresu URL to 2 MB.

  • Sprawdzanie funkcji: przed wywołaniem interfejsu API zawsze sprawdzaj dostępność funkcji za pomocą WebViewFeature.isFeatureSupported, aby zachować zgodność z różnymi wersjami APK WebView.

Inicjowanie nawigacji i śledzenie cyklu życia

Aby skonfigurować nawigację i śledzić jej cykl życia:

  1. Zarejestruj implementację NavigationListener za pomocą WebViewCompat.addNavigationListener podczas konfiguracji WebView, aby otrzymywać uporządkowane wywołania zwrotne cyklu życia. Zarejestruj detektor raz (a nie przy każdym wywołaniu nawigacji), aby zapobiec wyciekom pamięci i duplikowaniu wywołań zwrotnych.
  2. Utwórz instancję NavigationParameters za pomocą NavigationParameters.Builder, aby określić opcjonalne zachowania, takie jak zastępowanie historii lub niestandardowe nagłówki HTTP.
  3. Wywołaj WebViewCompat.navigate, przekazując instancję WebView, docelowy adres URL i parametry.

WebViewCompat.navigate zwraca obiekt Navigation, który jednoznacznie identyfikuje żądanie. W wywołaniach zwrotnych NavigationListener porównaj ten obiekt z przychodzącym parametrem Navigation, aby śledzić konkretną nawigację.

Przykład wdrożenia

Ten przykład pokazuje, jak skonfigurować parametry nawigacji, wywołać WebViewCompat.navigate i nasłuchiwać zdarzeń cyklu życia nawigacji:

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

Rodzaje błędów i obsługa błędów

Interfejs API WebViewCompat.navigate udostępnia odrębne mechanizmy obsługi błędów konfiguracji i błędów nawigacji w czasie działania:

Wyjątki nieprawidłowego argumentu

Przekazanie nieprawidłowych argumentów powoduje synchroniczne wywołanie IllegalArgumentException. Najczęstsze przyczyny:

  • Przekazanie wartości null dla wymaganych parametrów, które nie mogą mieć wartości null (webView, url lub params).
  • Podanie nieobsługiwanego schematu URI adresu URL, np. javascript:.
  • Przekazanie nieprawidłowo sformatowanych kluczy lub wartości nagłówków HTTP, które nie są zgodne ze specyfikacjami RFC 2616.

Jeśli podczas żądania sieciowego lub wczytywania strony wystąpi błąd (np. kod stanu HTTP 404, błąd rozpoznawania nazw DNS lub błąd protokołu SSL), WebViewCompat.navigate nadal zwraca prawidłowy obiekt Navigation.

Gdy nawigacja się zakończy, sprawdź te metody w Navigation instancji w wywołaniu zwrotnym onNavigationCompleted, aby zdiagnozować błąd:

  • getStatusCode: zwraca kod stanu odpowiedzi HTTP (np. 404 lub 500).
  • getWebResourceError: zwraca obiekt WebResourceErrorCompat zawierający szczegółowe informacje o błędach sieciowych, takich jak przekroczenie limitu czasu połączenia lub błędy wyszukiwania hosta.
  • didCommitErrorPage: wskazuje, czy WebView zatwierdził i wyświetlił użytkownikowi stronę błędu.
  • didCommit: wskazuje, czy nawigacja została pomyślnie zatwierdzona na stronie docelowej bez przerwania.

Zarządzanie pakietem stanu zapisu

Gdy przekazujesz dodatkowe nagłówki za pomocą NavigationParameters, WebView zapisuje te nagłówki w pakiecie stanu zapisu, aby można było ich ponownie użyć po przywróceniu stanu. Jednak duże zbiory nagłówków mogą znacznie zwiększyć rozmiar zapisanego stanu Bundle.

Jeśli musisz ograniczyć rozmiar pakietu, aby zapobiec wystąpieniu TransactionTooLargeException podczas zapisywania stanu Androida, użyj WebViewCompat.saveState. Ta metoda umożliwia ustawienie maksymalnego limitu rozmiaru pakietu w bajtach i opcjonalne wykluczenie elementów historii do przodu:

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

Wynikowy pakiet pozostaje zgodny ze standardową WebView.restoreState metodą.

Zalecenia dotyczące migracji i implementacji

Aby zapewnić optymalną wydajność i stabilność podczas nawigacji w WebView, postępuj zgodnie z tymi zaleceniami:

  • Migracja z loadUrl do navigate: przeprowadź migrację wszystkich starszych WebView.loadUrl wywołań do WebViewCompat.navigate. Zapewnia to jednolite zarządzanie historią i gwarantuje, że nagłówki są zawsze zapisywane jako część zapisanego stanu.

  • Zawsze sprawdzaj obsługę funkcji: przed wywołaniem interfejsu API sprawdź obsługę w czasie działania za pomocą WebViewFeature.isFeatureSupported, aby zabezpieczyć się przed starszymi wersjami WebView.

  • Korelacja instancji nawigacji: użyj zwróconego obiektu Navigation, aby odróżnić równoczesne nawigacje lub filtrować wywołania zwrotne podczas zarządzania wieloma instancjami WebView.

  • Zarejestruj detektor raz podczas inicjowania: ponieważ WebViewCompat.addNavigationListener dodaje detektor, a nie zastępuje istniejący, zarejestruj NavigationListener raz podczas WebView konfiguracji, aby uniknąć wycieków pamięci i duplikowania wywołań zwrotnych w kolejnych nawigacjach.

  • Monitoruj rozmiar stanu zapisu: podczas przekazywania dużych ładunków nagłówków używaj WebViewCompat.saveState z jawnymi granicami rozmiaru, aby uniknąć zapisywania nadmiernej ilości danych stanu.

Dodatkowe materiały

Więcej informacji o wbudowanych funkcjach internetowych i optymalizacji wydajności znajdziesz w tych przewodnikach: