Điều hướng trang nâng cao bằng WebViewCompat.navigate

WebViewCompat.navigate là một lựa chọn thay thế nâng cao cho WebView.loadUrl, giúp kiểm soát chi tiết quá trình tải trang web, quản lý nhật ký và theo dõi vòng đời điều hướng trong WebView.

Trước đây, việc bắt đầu điều hướng trang bằng loadUrl có những hạn chế đáng kể:

  • Không thay thế mục nhập nhật ký: Bạn không thể thay thế mục nhập nhật ký hiện tại, khiến bạn không thể chuyển đến một trang mới mà không cần thêm mục nhập vào ngăn xếp lui.
  • Các lệnh gọi lại tách biệt: Không có cơ chế trực tiếp nào để tương quan một lệnh gọi loadUrl cụ thể với các sự kiện gọi lại tiếp theo trong WebViewClient.
  • Không lưu tiêu đề bổ sung: Các tiêu đề tuỳ chỉnh được truyền đến loadUrl không được lưu dưới dạng một phần của trạng thái WebView, vì vậy, chúng sẽ bị mất khi khôi phục trạng thái.

API WebViewCompat.navigate giải quyết những vấn đề này bằng cách giới thiệu các tính năng sau:

  • Thay thế mục trong nhật ký điều hướng: Cho phép bạn thay thế trang hiện tại trong ngăn xếp nhật ký WebView.
  • Theo dõi lệnh gọi lại tương quan: Trả về một đối tượng Navigation đóng vai trò là giá trị nhận dạng duy nhất trong tất cả các giai đoạn của vòng đời điều hướng.
  • Hỗ trợ tiêu đề trạng thái đã lưu: Các tiêu đề bổ sung được lưu một cách đáng tin cậy trong gói trạng thái WebView để có thể dùng lại khi khôi phục trạng thái.

Các khả năng và hạn chế chính

Trước khi áp dụng WebViewCompat.navigate, hãy cân nhắc các quy tắc và hạn chế về hoạt động sau:

  • Độ an toàn cho luồng: Bạn phải gọi WebViewCompat.navigate trên luồng giao diện người dùng (chính).

  • Huỷ và thứ tự ưu tiên: Bạn không thể huỷ rõ ràng các chỉ dẫn trong chuyến bay. Tuy nhiên, việc bắt đầu một lệnh gọi navigate mới trên cùng một WebView sẽ thay thế mọi thao tác điều hướng đang hoạt động.

  • Hỗ trợ lược đồ URI: Lược đồ URI chuẩn (chẳng hạn như https:http:) và tuỳ chỉnh đều được hỗ trợ. Không hỗ trợ lược đồ javascript:.

  • Giới hạn kích thước URL: Độ dài chuỗi URL tối đa được hỗ trợ là 2 MB.

  • Kiểm tra tính năng: Luôn kiểm tra xem tính năng có dùng được hay không bằng cách sử dụng WebViewFeature.isFeatureSupported trước khi gọi API để duy trì khả năng tương thích trên nhiều phiên bản APK WebView.

Bắt đầu chế độ dẫn đường và theo dõi vòng đời

Để định cấu hình hoạt động điều hướng và theo dõi vòng đời của hoạt động này, hãy làm như sau:

  1. Đăng ký một quy trình triển khai NavigationListener bằng cách sử dụng WebViewCompat.addNavigationListener trong quá trình thiết lập WebView để nhận các lệnh gọi lại có cấu trúc trong vòng đời. Đăng ký trình nghe một lần (thay vì trên mọi lệnh gọi điều hướng) để ngăn tình trạng rò rỉ bộ nhớ và thực thi lệnh gọi lại trùng lặp.
  2. Tạo một thực thể NavigationParameters bằng cách sử dụng NavigationParameters.Builder để chỉ định các hành vi không bắt buộc, chẳng hạn như thay thế nhật ký hoặc tiêu đề HTTP tuỳ chỉnh.
  3. Gọi WebViewCompat.navigate, truyền thực thể WebView, URL đích và các tham số.

WebViewCompat.navigate trả về một đối tượng Navigation giúp xác định riêng biệt yêu cầu. Trong các lệnh gọi lại NavigationListener, hãy so sánh đối tượng này với tham số Navigation đến để theo dõi hoạt động điều hướng cụ thể đó.

Ví dụ về cách triển khai

Ví dụ sau đây minh hoạ cách định cấu hình các tham số điều hướng, gọi WebViewCompat.navigate và theo dõi các sự kiện trong vòng đời điều hướng:

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

Các chế độ lỗi và cách xử lý lỗi

API WebViewCompat.navigate cung cấp các cơ chế riêng biệt để xử lý lỗi cấu hình và lỗi điều hướng trong thời gian chạy:

Các trường hợp ngoại lệ liên quan đến đối số không hợp lệ

Việc truyền đối số không hợp lệ sẽ kích hoạt một IllegalArgumentException đồng bộ. Sau đây là một số nguyên nhân phổ biến:

  • Truyền null cho các tham số bắt buộc không rỗng (webView, url hoặc params).
  • Cung cấp một lược đồ URL không được hỗ trợ, chẳng hạn như javascript:.
  • Truyền các khoá hoặc giá trị tiêu đề HTTP không đúng định dạng, không tuân thủ quy cách RFC 2616.

Nếu xảy ra lỗi trong quá trình tải trang hoặc yêu cầu mạng (chẳng hạn như mã trạng thái HTTP 404, lỗi phân giải DNS hoặc lỗi SSL), WebViewCompat.navigate vẫn trả về một đối tượng Navigation hợp lệ.

Khi quá trình điều hướng kết thúc, hãy kiểm tra các phương thức sau trên thực thể Navigation bên trong lệnh gọi lại onNavigationCompleted để chẩn đoán lỗi:

  • getStatusCode: Trả về mã trạng thái phản hồi HTTP (ví dụ: 404 hoặc 500).
  • getWebResourceError: Trả về một đối tượng WebResourceErrorCompat trình bày chi tiết các lỗi mạng, chẳng hạn như hết thời gian chờ kết nối hoặc lỗi tra cứu máy chủ lưu trữ.
  • didCommitErrorPage: Cho biết liệu WebView có cam kết và hiển thị trang lỗi cho người dùng hay không.
  • didCommit: Cho biết liệu thao tác điều hướng có được chuyển thành công vào một trang đích mà không bị huỷ hay không.

Quản lý gói trạng thái đã lưu

Khi bạn truyền các tiêu đề bổ sung bằng NavigationParameters, WebView sẽ lưu các tiêu đề này trong gói trạng thái đã lưu để có thể dùng lại khi khôi phục trạng thái. Tuy nhiên, các tập hợp tiêu đề lớn có thể làm tăng đáng kể kích thước của trạng thái đã lưu Bundle.

Nếu cần hạn chế kích thước gói để ngăn TransactionTooLargeException trong quá trình lưu trạng thái Android, hãy dùng WebViewCompat.saveState. Phương thức này cho phép bạn đặt giới hạn kích thước gói tối đa tính bằng byte và tuỳ ý loại trừ các mục lịch sử chuyển tiếp:

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

Gói kết quả vẫn tương thích với phương thức WebView.restoreState tiêu chuẩn.

Đề xuất về việc di chuyển và triển khai

Để đảm bảo hiệu suất và độ ổn định tối ưu khi di chuyển trong WebView, hãy làm theo các đề xuất sau:

  • Di chuyển từ loadUrl sang navigate: Di chuyển tất cả các lệnh gọi WebView.loadUrl cũ sang WebViewCompat.navigate. Điều này đảm bảo việc quản lý nhật ký đồng nhất và đảm bảo tiêu đề luôn được lưu dưới dạng một phần của trạng thái đã lưu.

  • Luôn xác minh khả năng hỗ trợ tính năng: Trước khi gọi API, hãy xác nhận khả năng hỗ trợ thời gian chạy bằng WebViewFeature.isFeatureSupported để bảo vệ khỏi các phiên bản WebView cũ.

  • Tương quan các phiên bản điều hướng: Sử dụng đối tượng Navigation được trả về để phân biệt các thao tác điều hướng đồng thời hoặc lọc lệnh gọi lại khi quản lý nhiều phiên bản WebView.

  • Đăng ký trình nghe một lần trong quá trình khởi chạy:WebViewCompat.addNavigationListener thêm một trình nghe thay vì thay thế trình nghe hiện có, hãy đăng ký NavigationListener một lần trong quá trình thiết lập WebView để tránh rò rỉ bộ nhớ và thực thi lệnh gọi lại trùng lặp trong các thao tác điều hướng tiếp theo.

  • Theo dõi kích thước trạng thái đã lưu: Khi truyền tải trọng tiêu đề lớn, hãy sử dụng WebViewCompat.saveState với các ranh giới kích thước rõ ràng để tránh lưu dữ liệu trạng thái quá mức.

Tài nguyên khác

Để tìm hiểu thêm về các chức năng web được nhúng và cách tối ưu hoá hiệu suất, hãy xem các hướng dẫn sau: