การนำทางหน้าเว็บที่ปรับปรุงแล้วด้วย WebViewCompat.navigate

WebViewCompat.navigate เป็นทางเลือกที่ได้รับการปรับปรุงแทน WebView.loadUrl ซึ่งให้การควบคุมการโหลดหน้าเว็บ การจัดการประวัติ และการติดตามวงจรการนำทางใน WebView ได้อย่างละเอียด

ก่อนหน้านี้ การเริ่มการนำทางหน้าเว็บโดยใช้ loadUrl มีข้อจำกัดที่สำคัญดังนี้

  • ไม่มีการแทนที่รายการประวัติ: คุณไม่สามารถแทนที่รายการประวัติปัจจุบันได้ ทำให้ไม่สามารถไปยังหน้าใหม่ได้โดยไม่ต้องเพิ่มรายการลงในสแต็กย้อนกลับ
  • การเรียกกลับที่แยกออกจากกัน: ไม่มีกลไกโดยตรงในการเชื่อมโยงการเรียก loadUrl ที่เฉพาะเจาะจงกับเหตุการณ์การเรียกกลับที่ตามมาใน WebViewClient
  • ไม่ได้บันทึกส่วนหัวเพิ่มเติม: ระบบไม่ได้บันทึกส่วนหัวที่กำหนดเองซึ่งส่งไปยัง loadUrl เป็นส่วนหนึ่งของสถานะ WebView ดังนั้นส่วนหัวดังกล่าวจึงหายไปเมื่อกู้คืนสถานะ

WebViewCompat.navigate API แก้ไขปัญหาเหล่านี้ด้วยการเปิดตัวฟีเจอร์ต่อไปนี้

  • การแทนที่รายการประวัติการนำทาง: ให้คุณแทนที่หน้าปัจจุบัน ในสแต็กประวัติ WebView
  • การติดตามการเรียกกลับที่เชื่อมโยง: แสดงผลออบเจ็กต์ Navigation ที่ ทำหน้าที่เป็นตัวระบุที่ไม่ซ้ำกันในทุกขั้นตอนของวงจรการนำทาง
  • การรองรับส่วนหัวของสถานะที่บันทึกไว้: ระบบจะบันทึกส่วนหัวเพิ่มเติมไว้ใน WebView บันเดิลสถานะอย่างน่าเชื่อถือเพื่อให้ใช้ซ้ำได้เมื่อกู้คืนสถานะ

ความสามารถและข้อจำกัดที่สำคัญ

โปรดพิจารณากฎและข้อจำกัดในการปฏิบัติงานต่อไปนี้ก่อนใช้ WebViewCompat.navigate

  • ความปลอดภัยของเธรด: คุณต้องเรียกใช้ WebViewCompat.navigate ในเธรด UI (หลัก)

  • การยกเลิกและการแทนที่: คุณไม่สามารถยกเลิกการนำทางในเที่ยวบินได้โดยตรง อย่างไรก็ตาม การเริ่มnavigateการโทรใหม่ในWebView จะแทนที่การนำทางที่ใช้งานอยู่

  • การรองรับสคีม URI: ระบบรองรับสคีม URI มาตรฐาน (เช่น https: และ http:) และที่กำหนดเอง ไม่รองรับรูปแบบ javascript:

  • ขีดจำกัดขนาด URL: ความยาวสตริง URL สูงสุดที่รองรับคือ 2 MB

  • การตรวจสอบฟีเจอร์: ตรวจสอบความพร้อมใช้งานของฟีเจอร์โดยใช้ WebViewFeature.isFeatureSupported เสมอก่อนเรียกใช้ API เพื่อ รักษาความเข้ากันได้ใน WebView APK เวอร์ชันต่างๆ

เริ่มการนำทางและติดตามวงจร

หากต้องการกำหนดค่าการนำทางและติดตามวงจร ให้ทำดังนี้

  1. ลงทะเบียนการติดตั้งใช้งาน NavigationListener โดยใช้ WebViewCompat.addNavigationListener ระหว่างการตั้งค่า WebView เพื่อรับ การเรียกกลับวงจรที่มีโครงสร้าง ลงทะเบียน Listener เพียงครั้งเดียว (แทนที่จะลงทะเบียนใน การเรียกการนำทางทุกครั้ง) เพื่อป้องกันหน่วยความจำรั่วและป้องกันการเรียกใช้ การเรียกกลับที่ซ้ำกัน
  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 จะช่วยให้เซิร์ฟเวอร์แสดง UI ที่ปรับให้เหมาะกับแพลตฟอร์มหรือลิงก์ร้านค้าได้โดยไม่ต้องอาศัยการแยกวิเคราะห์สตริง 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);

โหมดความล้มเหลวและการจัดการข้อผิดพลาด

WebViewCompat.navigate API มีกลไกที่แตกต่างกันในการจัดการ ข้อผิดพลาดในการกำหนดค่าและการนำทางรันไทม์ที่ล้มเหลว ดังนี้

ข้อยกเว้นของอาร์กิวเมนต์ที่ไม่ถูกต้อง

การส่งอาร์กิวเมนต์ที่ไม่ถูกต้องจะทริกเกอร์ 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 ได้อย่างมาก

หากต้องการจำกัดขนาดของ 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 หลายรายการ

  • ลงทะเบียน Listener เพียงครั้งเดียวในระหว่างการเริ่มต้น: เนื่องจาก WebViewCompat.addNavigationListener จะเพิ่ม Listener แทนที่จะแทนที่ Listener ที่มีอยู่ ให้ลงทะเบียน NavigationListener เพียงครั้งเดียวในระหว่างการตั้งค่า WebView เพื่อหลีกเลี่ยงการรั่วไหลของหน่วยความจำและการเรียกใช้ฟังก์ชันเรียกกลับที่ซ้ำกันในการนำทางครั้งต่อๆ ไป

  • ตรวจสอบขนาดสถานะการบันทึก: เมื่อส่งเพย์โหลดส่วนหัวขนาดใหญ่ ให้ใช้ WebViewCompat.saveState ที่มีขอบเขตขนาดที่ชัดเจนเพื่อหลีกเลี่ยงการบันทึก ข้อมูลสถานะมากเกินไป

แหล่งข้อมูลเพิ่มเติม

ดูข้อมูลเพิ่มเติมเกี่ยวกับความสามารถของเว็บที่ฝังไว้และการเพิ่มประสิทธิภาพได้ในคำแนะนำต่อไปนี้