التنقّل المحسَّن في الصفحة باستخدام WebViewCompat.navigate

WebViewCompat.navigate هو بديل محسّن لـ WebView.loadUrl يوفّر تحكّمًا دقيقًا في تحميل صفحات الويب وإدارة السجلّ وتتبُّع مراحل نشاط التنقّل في WebView.

في السابق، كانت هناك قيود ملحوظة على بدء عمليات التنقّل في الصفحات باستخدام loadUrl:

  • عدم إمكانية استبدال إدخال السجلّ: لم يكن بإمكانك استبدال إدخال السجلّ الحالي، ما كان يجعل من المستحيل الانتقال إلى صفحة جديدة بدون إضافة إدخال إلى السجلّ الخلفي.
  • عمليات الاستدعاء غير المرتبطة: لم تتوفّر آلية مباشرة لربط طلب loadUrl معيّن بأحداث الاستدعاء اللاحقة في WebViewClient.
  • لم يتم حفظ العناوين الإضافية: لم يتم حفظ العناوين المخصّصة التي تم تمريرها إلى loadUrl كجزء من حالة WebView، وبالتالي تم فقدانها عند استعادة الحالة.

تعمل واجهة برمجة التطبيقات WebViewCompat.navigate على حلّ هذه المشاكل من خلال توفير الميزات التالية:

  • استبدال إدخال سجلّ التنقّل: يتيح لك استبدال الصفحة الحالية في حزمة سجلّ WebView.
  • تتبُّع عمليات معاودة الاتصال المرتبطة: تعرض هذه الطريقة عنصر Navigation يعمل كمعرّف فريد في جميع مراحل نشاط التنقّل.
  • إتاحة عناوين الحالة المحفوظة: يتم حفظ العناوين الإضافية بشكل موثوق في حزمة الحالة WebView حتى يمكن إعادة استخدامها عند استعادة الحالة.

الإمكانات والقيود الرئيسية

قبل استخدام WebViewCompat.navigate، يجب مراعاة قواعد التشغيل والقيود التالية:

  • أمان سلسلة التعليمات: يجب استدعاء WebViewCompat.navigate في سلسلة تعليمات واجهة المستخدم (الرئيسية).

  • الإلغاء والأولوية: لا يمكن إلغاء عمليات التنقّل أثناء الرحلة بشكل صريح. ومع ذلك، يؤدي بدء مكالمة navigate جديدة على WebView إلى إلغاء أي عملية تنقّل نشطة.

  • إتاحة مخطّطات URI: تتوفّر مخطّطات URI القياسية (مثل https: وhttp:) والمخصّصة. المخطط javascript: غير متاح.

  • الحدّ الأقصى لحجم عنوان URL: الحدّ الأقصى لطول سلسلة عنوان URL المسموح به هو 2 ميغابايت.

  • التحقّق من الميزة: تحقَّق دائمًا من توفّر الميزة باستخدام WebViewFeature.isFeatureSupported قبل استدعاء واجهة برمجة التطبيقات للحفاظ على التوافق مع إصدارات حِزم APK المختلفة من WebView.

بدء التنقّل وتتبُّع مراحل النشاط

لضبط التنقّل وتتبُّع مراحل نشاطه، اتّبِع الخطوات التالية:

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

طُرق التعذُّر ومعالجة الأخطاء

توفّر واجهة برمجة التطبيقات WebViewCompat.navigate آليات مختلفة للتعامل مع أخطاء الإعداد وحالات تعذُّر التنقّل في وقت التشغيل:

استثناءات الوسيطة غير الصالحة

يؤدي تمرير وسيطات غير صالحة إلى تشغيل IllegalArgumentException متزامن. تشمل الأسباب الشائعة ما يلي:

  • إدخال القيمة null للمَعلمات المطلوبة غير الفارغة (webView أو url أو params)
  • توفير مخطّط URL غير متوافق، مثل javascript:
  • تمرير مفاتيح أو قيم عناوين HTTP غير صالحة لا تتوافق مع مواصفات RFC 2616

في حال حدوث خطأ أثناء طلب شبكة أو تحميل صفحة (مثل رمز الحالة HTTP 404 أو تعذُّر التحويل باستخدام نظام أسماء النطاقات (DNS) أو خطأ في طبقة المقابس الآمنة)، يعرض 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. يضمن ذلك إدارة السجلّ بشكل موحّد ويضمن حفظ العناوين دائمًا كجزء من الحالة المحفوظة.

  • التأكّد دائمًا من توفّر الميزة: قبل استدعاء واجهة برمجة التطبيقات، تأكَّد من توفّرها في وقت التشغيل باستخدام WebViewFeature.isFeatureSupported للحماية من إصدارات WebView القديمة.

  • ربط حالات التنقّل: استخدِم العنصر Navigation الذي تم عرضه للتمييز بين عمليات التنقّل المتزامنة أو فلترة عمليات معاودة الاتصال عند إدارة عدة مثيلات WebView.

  • تسجيل أداة معالجة الأحداث مرة واحدة أثناء عملية التهيئة: لأنّ WebViewCompat.addNavigationListener تضيف أداة معالجة الأحداث بدلاً من استبدال أداة حالية، سجِّل NavigationListener مرة واحدة أثناء عملية إعداد WebView لتجنُّب تسرُّب الذاكرة وتنفيذ عمليات معاودة الاتصال المكرّرة في عمليات التنقّل اللاحقة.

  • مراقبة حجم حالة الحفظ: عند تمرير حمولات كبيرة للرؤوس، استخدِم WebViewCompat.saveState مع حدود حجم واضحة لتجنُّب حفظ بيانات حالة مفرطة.

مراجع إضافية

لمزيد من المعلومات حول إمكانات الويب المضمّنة وتحسين الأداء، يُرجى الاطّلاع على الأدلة التالية: