WebViewCompat.navigate によるページ ナビゲーションの強化

WebViewCompat.navigateWebView.loadUrl の強化版で、WebView でのウェブページの読み込み、履歴管理、ナビゲーション ライフサイクルのトラッキングをきめ細かく制御できます。

以前は、loadUrl を使用してページ ナビゲーションを開始する場合、次のような制限がありました。

  • 履歴エントリの置き換えなし: 現在の履歴エントリを置き換えることができなかったため、バックスタックにエントリを追加せずに新しいページに移動することができませんでした。
  • コールバックの分離: 特定の loadUrl 呼び出しと WebViewClient の後続のコールバック イベントを関連付ける直接的なメカニズムはありませんでした。
  • 追加のヘッダーが保存されない: loadUrl に渡されたカスタム ヘッダーが WebView 状態の一部として保存されなかったため、状態の復元時に失われました。

WebViewCompat.navigate API は、次の機能を導入することでこれらの問題を解決します。

  • ナビゲーション履歴エントリの置換: WebView 履歴スタック内の現在のページを置き換えることができます。
  • 相関コールバック トラッキング: ナビゲーション ライフサイクルのすべてのステージで一意の識別子として機能する Navigation オブジェクトを返します。
  • 保存済み状態ヘッダーのサポート: 状態の復元時に再利用できるように、追加のヘッダーが WebView 状態バンドルに確実に保存されます。

主な機能と制限事項

WebViewCompat.navigate を採用する前に、次の運用ルールと制約を考慮してください。

  • スレッドの安全性: UI(メイン)スレッドで WebViewCompat.navigate を呼び出す必要があります。

  • キャンセルと優先順位: 進行中のナビゲーションを明示的にキャンセルすることはできません。ただし、同じ WebView で新しい navigate 呼び出しを開始すると、アクティブなナビゲーションはすべて置き換えられます。

  • URI スキームのサポート: 標準(https:http: など)とカスタムの URI スキームがサポートされています。javascript: スキームはサポートされていません。

  • URL サイズの上限: サポートされている URL 文字列の最大長は 2 MB です。

  • 機能の確認: さまざまな WebView APK バージョン間の互換性を維持するため、API を呼び出す前に WebViewFeature.isFeatureSupported を使用して常に機能の可用性を確認してください。

ナビゲーションを開始してライフサイクルを追跡する

ナビゲーションを構成してライフサイクルを追跡するには、次の操作を行います。

  1. WebView のセットアップ中に WebViewCompat.addNavigationListener を使用して NavigationListener 実装を登録し、構造化されたライフサイクル コールバックを受け取ります。メモリリークとコールバックの重複実行を防ぐため、リスナーは 1 回だけ登録します(ナビゲーション呼び出しのたびに行うのではなく)。
  2. NavigationParameters.Builder を使用して NavigationParameters インスタンスを構築し、履歴の置換やカスタム 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);
    }
}

障害モードとエラー処理

WebViewCompat.navigate API は、構成エラーとランタイム ナビゲーション エラーを処理するための個別のメカニズムを提供します。

無効な引数の例外

無効な引数を渡すと、同期 IllegalArgumentException がトリガーされます。一般的な原因は次のとおりです。

  • 必須の非 null パラメータ(webViewurlparams)に null を渡している。
  • サポートされていない URL スキーム(javascript: など)を指定している。
  • RFC 2616 仕様に準拠していない形式の HTTP ヘッダーキーまたは値を渡す。

ネットワーク リクエストまたはページ読み込み中にエラー(HTTP 404 ステータス コード、DNS の解決の失敗、SSL エラーなど)が発生した場合でも、WebViewCompat.navigate は有効な Navigation オブジェクトを返します。

ナビゲーションが完了したら、onNavigationCompleted コールバック内の Navigation インスタンスで次のメソッドを調べて、障害を診断します。

  • getStatusCode: HTTP レスポンス ステータス コード(404500 など)を返します。
  • getWebResourceError: 接続タイムアウトやホストルックアップの失敗などのネットワーク エラーの詳細を示す WebResourceErrorCompat オブジェクトを返します。
  • didCommitErrorPage: WebView がコミットされ、ユーザーにエラーページが表示されたかどうかを示します。
  • didCommit: ナビゲーションが中止されずにターゲット ページに正常にコミットされたかどうかを示します。

状態バンドルの保存の管理

NavigationParameters で追加のヘッダーを渡すと、WebView はこれらのヘッダーを保存された状態バンドルに保存し、状態の復元時に再利用できるようにします。ただし、ヘッダーのコレクションが大きいと、保存された状態 Bundle のサイズが大幅に増加する可能性があります。

Android の状態保存中に TransactionTooLargeException が発生しないようにバンドルサイズを制限する必要がある場合は、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 バージョンから保護します。

  • ナビゲーション インスタンスを関連付ける: 複数の WebView インスタンスを管理する際に、返された Navigation オブジェクトを使用して、同時ナビゲーションを区別したり、コールバックをフィルタしたりします。

  • 初期化時にリスナーを 1 回登録する: WebViewCompat.addNavigationListener は既存のリスナーを置き換えるのではなく、リスナーを追加するため、WebView の設定時に NavigationListener を 1 回登録して、メモリリークと、後続のナビゲーションでのコールバックの重複実行を回避します。

  • 保存状態のサイズをモニタリングする: 大きなヘッダー ペイロードを渡す場合は、明示的なサイズ境界を持つ WebViewCompat.saveState を使用して、過剰な状態データの保存を回避します。

参考情報

埋め込みウェブ機能とパフォーマンスの最適化の詳細については、次のガイドをご覧ください。