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 wpisu 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 wstecznego.
  • Odłączone wywołania zwrotne:WebViewClient nie było bezpośredniego mechanizmu, który umożliwiałby powiązanie konkretnego wywołania loadUrl z kolejnymi zdarzeniami wywołania zwrotnego.
  • Dodatkowe nagłówki nie zostały zapisane: niestandardowe nagłówki przekazane do loadUrl nie zostały zapisane w ramach stanu WebView, więc zostały utracone podczas przywracania stanu.

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

  • Zastępowanie wpisu w historii nawigacji: umożliwia zastąpienie bieżącej strony w WebView stosie historii.
  • Śledzenie powiązanych 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 stanu zapisanego: dodatkowe nagłówki są niezawodnie zapisywane w pakiecie stanuWebView, dzięki czemu można ich używać ponownie po przywróceniu stanu.

Najważniejsze funkcje i ograniczenia

Zanim zaczniesz korzystać z WebViewCompat.navigate, weź pod uwagę te zasady i ograniczenia operacyjne:

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

  • Anulowanie i priorytet: nawigacji w trakcie lotu nie można wyraźnie anulować. Jednak rozpoczęcie nowego połączenia navigate na tym samym urządzeniu WebView zastępuje aktywną nawigację.

  • Obsługa schematów URI: obsługiwane są standardowe (np. https:http:) i 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ą metody WebViewFeature.isFeatureSupported, aby zachować zgodność z różnymi wersjami pliku APK WebView.

Rozpoczynanie 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ć strukturalne wywołania zwrotne dotyczące cyklu życia. Zarejestruj detektor raz (zamiast przy każdym wywołaniu nawigacji), aby zapobiec wyciekom pamięci i wykonywaniu duplikatów wywołań zwrotnych.
  2. Utwórz instancję NavigationParameters za pomocą funkcji NavigationParameters.Builder, aby określić opcjonalne zachowania, takie jak zastępowanie historii lub niestandardowe nagłówki HTTP.
  3. Wywołaj funkcję 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 implementacji

W przykładzie poniżej pokazujemy, jak skonfigurować parametry nawigacji, wywołać funkcję 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);
    }
}

Przekazywanie stanu aplikacji za pomocą nagłówków HTTP

Aplikacje internetowe często wymagają kontekstu z aplikacji na Androida, aby koordynować logikę backendu lub dostosowywać treści internetowe. Dołączanie parametrów zapytania do adresu URL w celu przekazywania tych informacji może powodować nieporządek w adresach URL, zakłócać buforowanie i ujawniać wewnętrzny stan aplikacji.

Zamiast tego zalecamy przekazywanie kontekstu aplikacji za pomocą niestandardowych nagłówków HTTP. Za pomocą funkcji WebViewCompat.navigateNavigationParameters możesz bezpiecznie wysyłać te dane na swój serwer. Dodatkowo komponent WebView zachowuje te nagłówki podczas przywracania stanu, co zapewnia spójność treści internetowych w przypadku zmian konfiguracji. Pamiętaj, że ta trwałość ma zastosowanie tylko w przypadku korzystania z WebViewCompat.navigate. Jeśli używasz WebView.loadUrl, niestandardowe nagłówki nie są zapisywane w pakiecie stanu WebView i zostaną utracone po przywróceniu.

Częste przypadki użycia

Typowe przypadki przekazywania kontekstu aplikacji hosta to:

  • Wersja aplikacji (X-App-Version): Przekazywanie wersji do publikacji aplikacji hosta (np. BuildConfig.VERSION_NAME) pomaga serwerowi backendu w weryfikacji zgodności natywnego mostu JavaScript, ograniczaniu dostępu do funkcji lub wyświetlaniu użytkownikom prośby o zaktualizowanie starszych aplikacji.
  • Platforma klienta (X-Client-Platform): wyraźne określenie środowiska hosta jako Androida umożliwia serwerowi dostarczanie interfejsu dostosowanego do platformy lub linków do sklepu z aplikacjami bez polegania na analizowaniu ciągu User-Agent.

Przykład implementacji

Ten przykład pokazuje, jak przekazać wersję aplikacji i platformę klienta na serwer WWW:

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

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

Interfejs WebViewCompat.navigate API 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 funkcji IllegalArgumentException. Oto najczęstsze przyczyny:

  • Przekazywanie wartości null w przypadku wymaganych parametrów, które nie mogą mieć wartości null (webView, url lub params).
  • Podanie nieobsługiwanego schematu URI adresu URL, np. javascript:.
  • Przekazywanie nieprawidłowych kluczy lub wartości nagłówka 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), funkcja WebViewCompat.navigate zwróci prawidłowy obiekt Navigation.

Po zakończeniu nawigacji sprawdź te metody w instancji Navigation 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 sieci, takich jak przekroczenie limitu czasu połączenia lub nieudane wyszukiwanie hosta.
  • didCommitErrorPage: wskazuje, czy WebView popełnił błąd i wyświetlił użytkownikowi stronę z komunikatem o błędzie.
  • didCommit: wskazuje, czy nawigacja została pomyślnie przekierowana na stronę docelową bez przerwania.

Zarządzanie pakietem stanu zapisu

Gdy przekażesz dodatkowe nagłówki za pomocą NavigationParameters, WebView zapisze je w pakiecie stanu zapisanego, aby można było ich użyć ponownie podczas przywracania stanu. Jednak duże zbiory nagłówków mogą znacznie zwiększyć rozmiar zapisanego stanuBundle.

Jeśli chcesz ograniczyć rozmiar pakietu, aby zapobiec TransactionTooLargeException podczas zapisywania stanu Androida, użyj WebViewCompat.saveState. Ta metoda pozwala ustawić maksymalny limit rozmiaru pakietu w bajtach i opcjonalnie wykluczyć elementy 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ą metodą WebView.restoreState.

Zalecenia dotyczące migracji i wdrażania

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

  • Migracja z loadUrl do navigate: przenieś wszystkie połączenia starszego typu WebView.loadUrl 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 zdarzeń raz podczas inicjowania: ponieważ funkcja WebViewCompat.addNavigationListener dodaje detektor zdarzeń zamiast zastępować istniejący, zarejestruj funkcję NavigationListener raz podczas WebViewkonfiguracji, aby uniknąć wycieków pamięci i wykonywania duplikatów wywołań zwrotnych podczas kolejnych nawigacji.

  • Monitoruj rozmiar stanu zapisu: podczas przekazywania dużych ładunków nagłówka używaj funkcji WebViewCompat.saveState z określonymi granicami rozmiaru, aby uniknąć zapisywania nadmiernych danych stanu.

Dodatkowe materiały

Więcej informacji o osadzonych uprawnieniach witryn i optymalizacji wydajności znajdziesz w tych przewodnikach: