Улучшенная навигация по страницам с помощью WebViewCompat.navigate

WebViewCompat.navigate – это улучшенная альтернатива WebView.loadUrl, которая обеспечивает точный контроль над загрузкой веб-страниц, управлением историей и отслеживанием жизненного цикла навигации в WebView.

Ранее у навигации по страницам с помощью loadUrl были существенные ограничения:

  • Невозможность замены записи в истории. Вы не могли заменить текущую запись в истории, поэтому нельзя было перейти на новую страницу, не добавив запись в обратный стек.
  • Несвязанные обратные вызовы. Не было прямого механизма, позволяющего соотнести определенный вызов loadUrl с последующими событиями обратного вызова в WebViewClient.
  • Дополнительные заголовки не сохраняются. Специальные заголовки, переданные в loadUrl, не сохранялись как часть состояния WebView, поэтому они терялись при восстановлении состояния.

WebViewCompat.navigate API решает эти проблемы, предлагая следующие функции:

  • Замена записи в истории навигации. Позволяет заменить текущую страницу в истории WebView.
  • Отслеживание связанных обратных вызовов. Возвращает объект Navigation, который служит уникальным идентификатором на всех этапах жизненного цикла навигации.
  • Поддержка заголовков сохраненного состояния. Дополнительные заголовки надежно сохраняются в пакете состояния WebView, чтобы их можно было использовать повторно при восстановлении состояния.

Основные возможности и ограничения

Прежде чем начать использовать WebViewCompat.navigate, ознакомьтесь со следующими правилами и ограничениями:

  • Потокобезопасность. Метод WebViewCompat.navigate необходимо вызывать в потоке пользовательского интерфейса (основном).

  • Отмена и приоритет. Выполняемую навигацию нельзя отменить явным образом. Однако новый вызов navigate для того же WebView отменяет активную навигацию.

  • Поддержка схем URI. Поддерживаются стандартные (например, https: и http:) и специальные схемы URI. Схема javascript: не поддерживается.

  • Ограничение на размер URL. Максимальная длина строки URL – 2 МБ.

  • Проверка функций. Чтобы обеспечить совместимость с разными версиями APK-файла WebView, всегда проверяйте доступность функций с помощью WebViewFeature.isFeatureSupported, прежде чем вызывать API.

Запуск навигации и отслеживание жизненного цикла

Чтобы настроить навигацию и отслеживать ее жизненный цикл, выполните следующие действия:

  1. Зарегистрируйте реализацию NavigationListener с помощью WebViewCompat.addNavigationListener во время настройки WebView, чтобы получать структурированные обратные вызовы жизненного цикла. Зарегистрируйте прослушиватель один раз (а не при каждом вызове навигации), чтобы предотвратить утечки памяти и дублирование обратных вызовов.
  2. Создайте экземпляр NavigationParameters с помощью NavigationParameters.Builder, чтобы указать дополнительные действия, например замену истории или специальные HTTP-заголовки.
  3. Вызовите метод WebViewCompat.navigate, передав экземпляр WebView, целевой URL и параметры.

WebViewCompat.navigate возвращает объект Navigation, который уникальным образом идентифицирует запрос. В обратных вызовах NavigationListener сравните этот объект с входящим параметром Navigation, чтобы отслеживать конкретный переход.

Пример реализации

В примере ниже показано, как настроить параметры навигации, вызвать WebViewCompat.navigate и прослушивать события жизненного цикла навигации.

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

Передача состояния приложения с помощью HTTP-заголовков

Веб-приложениям часто требуется контекст из хост-приложения Android для координации внутренней логики или настройки веб-контента. Добавление параметров запроса в URL-адрес для передачи этой информации может загромождать URL-адреса, мешать кэшированию и раскрывать внутреннее состояние приложения.

Вместо этого мы рекомендуем передавать контекст приложения с помощью специальных HTTP-заголовков. С помощью WebViewCompat.navigate и NavigationParameters вы можете безопасно отправлять эти данные на свой сервер. Кроме того, WebView сохраняет эти заголовки при восстановлении состояния, что обеспечивает согласованность веб-контента при изменении конфигурации. Обратите внимание, что это правило действует только при использовании WebViewCompat.navigate. Если вы используете WebView.loadUrl, специальные заголовки не сохраняются в пакете состояния WebView и теряются при восстановлении.

Примеры использования

Ниже приведены распространенные примеры использования контекста основного приложения.

  • Версия приложения (X-App-Version). Передача версии приложения (например, BuildConfig.VERSION_NAME) помогает бэкенд-серверу проверять совместимость нативного моста JavaScript, ограничивать доступ к функциям или предлагать пользователям обновить устаревшие приложения.
  • Платформа клиента (X-Client-Platform). Явное указание на то, что хост-средой является Android, позволяет серверу предоставлять интерфейс, адаптированный для платформы, или направлять ссылки на магазин без необходимости анализировать строку User-Agent.

Пример реализации

В примере ниже показано, как передать веб-серверу версию приложения и платформу клиента.

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

Режимы сбоев и обработка ошибок

В API WebViewCompat.navigate предусмотрены отдельные механизмы для обработки ошибок конфигурации и сбоев навигации во время выполнения.

Исключения недопустимых аргументов

Передача недопустимых аргументов вызывает синхронную ошибку IllegalArgumentException. Обычно это происходит в следующих случаях:

  • Передача значения null для обязательных параметров, которые не могут быть нулевыми (webView, url или params).
  • Неподдерживаемая схема URL, например javascript:.
  • Передача неправильно сформированных ключей или значений HTTP-заголовков, не соответствующих спецификациям RFC 2616.

Если во время сетевого запроса или загрузки страницы произойдет сбой (например, код статуса HTTP 404, сбой DNS-преобразования или ошибка SSL), WebViewCompat.navigate все равно вернет действительный объект Navigation.

Когда навигация завершится, проверьте следующие методы в экземпляре Navigation внутри обратного вызова onNavigationCompleted, чтобы диагностировать сбой:

  • getStatusCode – возвращает код статуса HTTP-ответа (например, 404 или 500).
  • getWebResourceError – возвращает объект WebResourceErrorCompat с подробными сведениями об ошибках сети, например о превышении времени ожидания подключения или сбоях поиска хоста.
  • didCommitErrorPage – указывает, зафиксировал ли WebView ошибку и показал ли пользователю страницу ошибки.
  • didCommit – указывает, был ли переход на целевую страницу выполнен успешно или прерван.

Управление пакетами сохранения состояния

Когда вы передаете дополнительные заголовки с помощью NavigationParameters, WebView сохраняет их в наборе сохраненных состояний, чтобы их можно было использовать повторно при восстановлении состояния. Однако большие наборы заголовков могут существенно увеличить размер сохраненного состояния Bundle.

Если вам нужно ограничить размер пакета, чтобы предотвратить TransactionTooLargeException при сохранении состояния Android, используйте WebViewCompat.saveState. Этот метод позволяет задать максимальный размер пакета в байтах и при необходимости исключить элементы истории пересылки.

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

Полученный пакет совместим со стандартным методом WebView.restoreState.

Рекомендации по переносу и внедрению

Чтобы обеспечить оптимальную производительность и стабильность при навигации в WebView, следуйте этим рекомендациям:

  • Переход с loadUrl на navigate. Перенесите все устаревшие звонки WebView.loadUrl в WebViewCompat.navigate. Это обеспечивает единообразное управление историей и гарантирует, что заголовки всегда сохраняются как часть сохраненного состояния.

  • Всегда проверяйте поддержку функций. Прежде чем вызывать API, убедитесь, что он поддерживается в среде выполнения, с помощью WebViewFeature.isFeatureSupported. Это позволит избежать проблем со старыми версиями WebView.

  • Сопоставление экземпляров навигации. Используйте возвращенный объект Navigation, чтобы различать параллельные навигации или фильтровать обратные вызовы при управлении несколькими экземплярами WebView.

  • Зарегистрируйте прослушиватель один раз во время инициализации. Поскольку WebViewCompat.addNavigationListener добавляет прослушиватель, а не заменяет существующий, зарегистрируйте NavigationListener один раз во время настройки WebView, чтобы избежать утечек памяти и дублирования обратных вызовов при последующих переходах.

  • Отслеживайте размер сохраняемого состояния. При передаче больших полезных нагрузок заголовков используйте WebViewCompat.saveState с явными границами размера, чтобы избежать сохранения избыточных данных о состоянии.

Дополнительные ресурсы

Чтобы узнать больше о встроенных веб-возможностях и оптимизации производительности, ознакомьтесь со следующими руководствами: