Navigation améliorée sur les pages avec WebViewCompat.navigate

WebViewCompat.navigate est une alternative améliorée à WebView.loadUrl qui permet de contrôler précisément le chargement des pages Web, la gestion de l'historique et le suivi du cycle de vie de la navigation dans WebView.

Auparavant, l'initiation de la navigation sur les pages à l'aide de loadUrl présentait des limites importantes :

  • Aucun remplacement de l'entrée d'historique : vous n'avez pas pu remplacer l'entrée d'historique actuelle, ce qui vous a empêché d'accéder à une nouvelle page sans ajouter d'entrée à la pile "Retour".
  • Rappels découplés : il n'existait aucun mécanisme direct permettant de corréler un appel loadUrl spécifique avec les événements de rappel ultérieurs dans WebViewClient.
  • En-têtes supplémentaires non enregistrés : les en-têtes personnalisés transmis à loadUrl n'ont pas été enregistrés dans l'état WebView. Ils ont donc été perdus lors de la restauration de l'état.

L'API WebViewCompat.navigate résout ces problèmes en proposant les fonctionnalités suivantes :

  • Remplacement d'une entrée de l'historique de navigation : vous permet de remplacer la page actuelle dans la pile de l'historique WebView.
  • Suivi des rappels corrélés : renvoie un objet Navigation qui sert d'identifiant unique à toutes les étapes d'un cycle de vie de navigation.
  • Prise en charge de l'en-tête d'état enregistré : les en-têtes supplémentaires sont enregistrés de manière fiable dans le bundle d'état WebView afin de pouvoir être réutilisés lors de la restauration de l'état.

Principales fonctionnalités et limites

Avant d'adopter WebViewCompat.navigate, tenez compte des règles et contraintes opérationnelles suivantes :

  • Sécurité des threads : vous devez appeler WebViewCompat.navigate sur le thread UI (principal).

  • Annulation et priorité : les navigations en cours ne peuvent pas être annulées explicitement. Toutefois, le lancement d'un nouvel appel navigate sur le même WebView remplace toute navigation active.

  • Compatibilité avec les schémas d'URI : les schémas d'URI standards (tels que https: et http:) et personnalisés sont acceptés. Le schéma javascript: n'est pas accepté.

  • Limite de taille des URL : la longueur maximale de la chaîne d'URL acceptée est de 2 Mo.

  • Vérification des fonctionnalités : vérifiez toujours la disponibilité des fonctionnalités à l'aide de WebViewFeature.isFeatureSupported avant d'appeler l'API pour maintenir la compatibilité entre les différentes versions APK de WebView.

Lancer la navigation et suivre le cycle de vie

Pour configurer la navigation et suivre son cycle de vie, procédez comme suit :

  1. Enregistrez une implémentation NavigationListener à l'aide de WebViewCompat.addNavigationListener lors de la configuration de WebView pour recevoir des rappels de cycle de vie structurés. Enregistrez l'écouteur une seule fois (plutôt qu'à chaque appel de navigation) pour éviter les fuites de mémoire et les exécutions de rappel en double.
  2. Construisez une instance NavigationParameters à l'aide de NavigationParameters.Builder pour spécifier des comportements facultatifs, tels que le remplacement de l'historique ou des en-têtes HTTP personnalisés.
  3. Appelez WebViewCompat.navigate en transmettant votre instance WebView, l'URL de destination et les paramètres.

WebViewCompat.navigate renvoie un objet Navigation qui identifie de manière unique la requête. Dans vos rappels NavigationListener, comparez cet objet avec le paramètre Navigation entrant pour suivre cette navigation spécifique.

Exemple de mise en œuvre

L'exemple suivant montre comment configurer les paramètres de navigation, appeler WebViewCompat.navigate et écouter les événements de cycle de vie de la navigation :

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

Propager l'état de l'application à l'aide d'en-têtes HTTP

Les applications Web ont souvent besoin du contexte de l'application hôte Android pour coordonner la logique du backend ou personnaliser le contenu Web. L'ajout de paramètres de requête à l'URL pour transmettre ces informations peut encombrer les URL, interférer avec la mise en cache et exposer l'état interne de l'application.

Nous vous recommandons plutôt de transmettre le contexte de l'application à l'aide d'en-têtes HTTP personnalisés. En utilisant WebViewCompat.navigate et NavigationParameters, vous pouvez envoyer ces données de manière sécurisée à votre serveur. De plus, WebView conserve ces en-têtes lors de la restauration de l'état, ce qui garantit que le contenu Web reste cohérent lors des modifications de configuration. Notez que cette persistance ne s'applique que lorsque vous utilisez WebViewCompat.navigate. Si vous utilisez WebView.loadUrl, les en-têtes personnalisés ne sont pas enregistrés dans le bundle d'état WebView et sont perdus lors de la restauration.

Cas d'utilisation courants

Voici quelques cas d'utilisation courants pour transmettre le contexte de l'application hôte :

  • Version de l'application (X-App-Version) : le fait de transmettre la version de publication de l'application hôte (par exemple, BuildConfig.VERSION_NAME) aide votre serveur backend à vérifier la compatibilité du pont JavaScript natif, à limiter l'accès à certaines fonctionnalités ou à inviter les utilisateurs à mettre à jour les anciennes applications.
  • Plate-forme client (X-Client-Platform) : l'identification explicite de l'environnement hôte en tant qu'Android permet au serveur de fournir une UI adaptée à la plate-forme ou des liens vers la plate-forme de téléchargement d'applications sans s'appuyer sur l'analyse de la chaîne User-Agent.

Exemple de mise en œuvre

L'exemple suivant montre comment transmettre la version de l'application et la plate-forme client à un serveur 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);

Modes de défaillance et gestion des erreurs

L'API WebViewCompat.navigate fournit des mécanismes distincts pour gérer les erreurs de configuration et les échecs de navigation lors de l'exécution :

Exceptions d'arguments non valides

La transmission d'arguments non valides déclenche une IllegalArgumentException synchrone. Voici quelques causes courantes :

  • Transmettre null pour les paramètres obligatoires non nuls (webView, url ou params).
  • Fournir un schéma d'URL non compatible, tel que javascript:.
  • Transmettre des clés ou des valeurs d'en-tête HTTP mal formées qui ne sont pas conformes aux spécifications RFC 2616.

En cas d'échec de la requête réseau ou du chargement de la page (par exemple, code d'état HTTP 404, échec de la résolution DNS ou erreur SSL), WebViewCompat.navigate renvoie toujours un objet Navigation valide.

Une fois la navigation terminée, inspectez les méthodes suivantes sur l'instance Navigation dans votre rappel onNavigationCompleted pour diagnostiquer l'échec :

  • getStatusCode : renvoie le code d'état de la réponse HTTP (par exemple, 404 ou 500).
  • getWebResourceError : renvoie un objet WebResourceErrorCompat détaillant les erreurs réseau, telles que les délais d'inactivité de connexion ou les échecs de recherche d'hôte.
  • didCommitErrorPage : indique si WebView a validé et affiché une page d'erreur à l'utilisateur.
  • didCommit : indique si la navigation a bien été effectuée sur une page cible sans être interrompue.

Gestion des groupes d'état enregistrés

Lorsque vous transmettez des en-têtes supplémentaires avec NavigationParameters, WebView enregistre ces en-têtes dans son bundle d'état enregistré afin qu'ils puissent être réutilisés lors de la restauration de l'état. Toutefois, les grandes collections d'en-têtes peuvent augmenter considérablement la taille de l'état enregistré Bundle.

Si vous devez limiter la taille du bundle pour éviter TransactionTooLargeException lors des enregistrements d'état Android, utilisez WebViewCompat.saveState. Cette méthode vous permet de définir une limite de taille maximale pour le bundle (en octets) et d'exclure éventuellement les éléments de l'historique "Précédent" :

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

Le bundle obtenu reste compatible avec la méthode standard WebView.restoreState.

Recommandations de migration et d'implémentation

Pour garantir des performances et une stabilité optimales lorsque vous naviguez dans WebView, suivez ces recommandations :

  • Migrer de loadUrl vers navigate : migrez tous les anciens appels WebView.loadUrl vers WebViewCompat.navigate. Cela garantit une gestion uniforme de l'historique et assure que les en-têtes sont toujours enregistrés dans l'état enregistré.

  • Vérifiez toujours la compatibilité des fonctionnalités : avant d'appeler l'API, vérifiez la compatibilité de l'exécution avec WebViewFeature.isFeatureSupported pour vous prémunir contre les anciennes versions de WebView.

  • Corréler les instances de navigation : utilisez l'objet Navigation renvoyé pour différencier les navigations simultanées ou filtrer les rappels lorsque vous gérez plusieurs instances WebView.

  • Enregistrez l'écouteur une seule fois lors de l'initialisation : comme WebViewCompat.addNavigationListener ajoute un écouteur au lieu de remplacer un écouteur existant, enregistrez votre NavigationListener une seule fois lors de la configuration de WebView pour éviter les fuites de mémoire et les exécutions de rappel en double lors des navigations ultérieures.

  • Surveillez la taille de l'état de sauvegarde : lorsque vous transmettez de grandes charges utiles d'en-tête, utilisez WebViewCompat.saveState avec des limites de taille explicites pour éviter d'enregistrer des données d'état excessives.

Ressources supplémentaires

Pour en savoir plus sur les fonctionnalités Web intégrées et l'optimisation des performances, consultez les guides suivants :