التنقل المؤجل (Deferred Navigation) هو نمط من التنقل المتأخر حيث يحدث الانتقال إلى الشاشة التالية بعد اكتمال عملية غير متزامنة، بدلاً من أن يحدث مباشرة في لحظة إجراء المستخدم. وفقًا لـ Android Developers (2024)، يتجنب التنقل المؤجل حالة السباق بين التنقل وتحميل البيانات، ويبسط معالجة الانتقالات من الإشعارات الفورية والروابط العميقة. الفرق الرئيسي — يتم حساب المسار بعد توفر جميع البيانات الضرورية.
النقاط الرئيسية
التنقل المؤجل (Deferred Navigation) هو نمط معماري حيث يتم تأجيل قرار التنقل حتى تصبح جميع البيانات الضرورية متاحة. على عكس الانتقال المباشر حيث يضغط المستخدم على زر ويدخل فورًا إلى شاشة جديدة، يفصل التنقل المؤجل بين حدث المشغل والانتقال الفعلي بوضع عملية غير متزامنة بينهما.
معماريًا، يُبنى التنقل المؤجل على تغيير الحالة: ضغط زر يبدأ عملية غير متزامنة، والاشتراك في نتيجتها يشغل التنقل. هذا مهم بشكل خاص في التطبيقات ذات البنية MVVM أو MVI، حيث تدير ViewModel الحالة وتشترك View (Activity أو Fragment أو SwiftUI View) في التغييرات وتستجيب بالانتقال. يزيل هذا النهج الاعتماد المباشر بين واجهة المستخدم ومنطق التنقل.
وفقًا لـ Google I/O 2023، يُوصى بالتنقل المؤجل لجميع السيناريوهات التي يعتمد فيها التنقل على نتيجة طلب شبكة أو التحقق من المصادقة أو تحميل التكوين أو الأذونات. النمط إلزامي أيضًا عند معالجة الروابط العميقة، حيث يجب أن يبدأ التطبيق أولاً ويحمل الشاشة الجذرية وبعدها فقط ينتقل إلى المسار المستهدف.
التنقل المؤجل يُستخدم في السيناريوهات التي يؤدي فيها التنقل المباشر إلى حالة شاشة غير صحيحة أو أخطاء في التحميل. دعنا نستعرض أربع حالات رئيسية حيث يكون التنقل المؤجل ضروريًا.
إذا نقر المستخدم على محتوى محمي، يجب على التطبيق أولاً التحقق من رمز الوصول. التنقل المباشر إلى شاشة المحتوى سيؤدي إلى شاشة فارغة أو خطأ 401 إذا انتهت صلاحية الرمز. يتحقق التنقل المؤجل من الرمز، وعند النجاح فقط — ينتقل إلى الشاشة المستهدفة. عند الفشل — يعيد التوجيه إلى شاشة تسجيل الدخول.
عند فتح التطبيق عبر رابط خارجي، يجب أولاً تحميل الشاشة الجذرية واستعادة حالة التنقل وبعدها فقط تنفيذ الانتقال عبر الرابط العميق. التنقل المباشر إلى الشاشة المستهدفة دون سياق جذري سيؤدي إلى حالات شاذة: مكدس تنقل فارغ أو مكدس رجوع مكسور.
عند النقر على إشعار فوري، يمكن أن يكون التطبيق في إحدى ثلاث حالات: مغلق أو في الخلفية أو نشط. يحدد التنقل المؤجل حالة التطبيق ويحمل المحتوى الضروري وبعدها فقط يعرض الشاشة المستهدفة. يسمح iOS بمعالجة هذا السيناريو من خلال UNNotificationContentExtension.
إذا كانت وظائف الشاشة مُدارة بواسطة علامة ميزة من الخادم، يسمح التنقل المؤجل أولاً بطلب التكوين وبعدها فقط بعرض الشاشة. إذا كانت الميزة معطلة — يرى المستخدم محتوى بديلاً أو عنصر نائب بدلاً من شاشة فارغة.
| السيناريو | التنقل المباشر | التنقل المؤجل |
|---|---|---|
| التفويض | شاشة فارغة مع رمز منتهي الصلاحية | إعادة التوجيه إلى تسجيل الدخول |
| الرابط العميق | مكدس رجوع مكسور | مكدس تنقل صحيح |
| الإشعار الفوري | تحميل بدون سياق | البيانات جاهزة قبل الانتقال |
| علامة الميزة | عرض وظيفة غير متاحة | عنصر نائب أو بديل |
التنقل المباشر هو نهج تقليدي حيث يتم الانتقال فورًا استجابةً لحدث. يضغط المستخدم على زر، ويقوم موجه واجهة المستخدم بتبديل الشاشة فورًا. هذا النهج بسيط ومتوقع ولكنه محدود في السيناريوهات التي تتطلب بيانات من الخادم أو التحقق من الشروط.
التنقل المؤجل يضيف طبقة على شكل حالة غير متزامنة. حدث المستخدم يبدأ عملية، والاشتراك في النتيجة يتحكم في التنقل. هذا يزيد من تعقيد الشفرة لكنه يوفر مرونة: نفس المشغل يمكن أن يؤدي إلى شاشات مختلفة اعتمادًا على البيانات المحملة.
الاختيار بين النهجين يعتمد على المتطلبات: إذا كان عرض الشاشة لا يتطلب بيانات غير متزامنة — استخدم التنقل المباشر. إذا كانت الشاشة تعتمد على نتيجة طلب أو مصادقة أو شروط خارجية — التنقل المؤجل ضروري. النهج الهجين، حيث بعض الانتقالات مباشرة وبعضها مؤجل، هو الممارسة الأكثر شيوعًا في التطبيقات الصناعية.
Android Jetpack يوفر آليات لتنفيذ التنقل المؤجل على مستوى البنية. الفكرة الرئيسية — ViewModel تدير الحالة، بينما Activity أو Fragment تشترك في التغييرات وتشغل التنقل عبر NavController.
StateFlow في coroutines في Kotlin هو الأداة المثالية للتنقل المؤجل. تقوم ViewModel بتحديث StateFlow بحدث تنقل، وتقوم Activity بمراقبته وتنفيذ الانتقال. عندما تتم معالجة الحدث، يتم مسح StateFlow، مما يمنع التنقل المتكرر.
class MainViewModel : ViewModel() {
private val _navigation = MutableSharedFlow<NavigationEvent>()
val navigation: SharedFlow<NavigationEvent> = _navigation
fun onDeepLinkReceived(link: String) {
viewModelScope.launch {
val data = resolveDeepLink(link)
_navigation.emit(NavigationEvent.GoToScreen(data))
}
}
}
في Activity، الاشتراك في التنقل يشغل NavController مع المسار من ViewModel. لمنع التنقل المتكرر عند تدوير الشاشة، يُستخدم غلاف NavigationEventWrapper الذي يعالج الحدث مرة واحدة فقط. يدعم Jetpack Navigation 2.7+ Safe Args لنقل الوسائط بشكل آمن من حيث النوع.
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val vm: MainViewModel by viewModels()
repeatOnLifecycle(Lifecycle.State.STARTED) {
vm.navigation.collect { event ->
when (event) {
is NavigationEvent.GoToScreen ->
findNavController(R.id.nav_host)
.navigate(event.route)
}
}
}
}
}
iOS لا يحتوي على Navigation Component مدمج مشابه لـ Android Jetpack، لذلك ينفذ المطورون التنقل المؤجل من خلال Coordinator Pattern بالاشتراك مع Combine أو async/await. يدير Coordinator مكدس الشاشات ويتخذ قرارات التنقل بناءً على البيانات المحملة.
Coordinator هو كائن يدير التنقل بين ViewControllers. بالاشتراك مع Combine، تقوم ViewModel بنشر الأحداث عبر PassthroughSubject، ويشترك Coordinator فيها وينفذ الانتقال. يفصل هذا النهج تمامًا واجهة المستخدم عن منطق التنقل ويتبع توصيات Apple لبنية التطبيقات.
final class AppCoordinator {
private var cancellables = Set<AnyCancellable>()
func start(viewModel: MainViewModel) {
viewModel.$navigationDestination
.compactMap { $0 }
.sink { [weak self] destination in
self?.navigateTo(destination)
}
.store(in: &cancellables)
}
}
Swift 5.5 قدم التزامن المنظم، الذي يسمح بتنفيذ التنقل المؤجل عبر async/await بدون Combine. توفر ViewModel دالة async تعيد مسارًا بعد تحميل البيانات. يستدعي Coordinator هذه الدالة في Task وينفذ الانتقال بناءً على المسار المستلم.
class AuthViewModel: ObservableObject {
func resolveDeeplink(_ url: URL) async -> AppRoute? {
guard let token = await AuthService.shared.getValidToken() else { return .login }
return await DeeplinkRouter.resolve(url, token: token)
}
}
// In Coordinator:
Task {
if let route = await viewModel.resolveDeeplink(url) {
navigateTo(route)
}
}
التنقل المؤجل يبسط معالجة السيناريوهات غير المتزامنة لكنه يتطلب نهجًا منضبطًا لإدارة الحالة. دعنا نستعرض الأخطاء الرئيسية التي يرتكبها المطورون عند تطبيق التنقل المؤجل.
الخطأ الأكثر شيوعًا — محاولة تنفيذ التنقل المؤجل قبل أن تتم تهيئة الشاشة الجذرية بالكامل ويكون NavController أو Coordinator جاهزًا للانتقال. في أندرويد يؤدي هذا إلى IllegalStateException، في iOS — إلى حالة واجهة مستخدم غير محددة. الحل — التأكد من أن دورة حياة المكون في حالة STARTED أو RESUMED قبل تشغيل التنقل.
إذا كان StateFlow أو Subject لا يمسح الحدث بعد المعالجة، عند العودة إلى الشاشة السابقة قد يتم إعادة توجيه المستخدم تلقائيًا إلى نفس الشاشة مرة أخرى. استخدم SharedFlow مع replay=0 في أندرويد أو CurrentValueSubject مع nil بعد المعالجة في iOS، بحيث يتم تشغيل حدث التنقل مرة واحدة فقط.
استخدم مكونًا مركزيًا واحدًا لجميع التنقلات في التطبيق. عندما يكون لكل Activity أو Fragment أو ViewController وحدة تحكم تنقل خاصة بها، يصبح التنقل المؤجل بين أجزاء مختلفة من التطبيق فوضويًا. Coordinator واحد يسهل تصحيح الأخطاء واختبار سيناريوهات التنقل.
التنقل المؤجل أصعب في الاختبار من التنقل المباشر لأن العمليات غير المتزامنة تُدخل عامل الوقت. استخدم TestDispatcher في أندرويد (kotlinx-coroutines-test) وXCTestExpectation في iOS لمحاكاة تحميل البيانات والتحقق من أن التنقل يتم وفقًا للمسار المتوقع. قم بمحاكاة خدمات المصادقة والروابط العميقة لاختبار معزول لكل سيناريو.
الأسئلة الشائعة
التنقل المؤجل هو نمط انتقال متأخر يمكن تطبيقه في أي سيناريو غير متزامن. الرابط العميق هو أحد مشغلات التنقل المؤجل، لكنه ليس الوحيد. المصادقة وعلامات الميزات تستخدم أيضًا التنقل المؤجل.
نعم، معظم التطبيقات تستخدم نهجًا هجينًا. شاشة قائمة المنتجات (بدون تبعيات غير متزامنة) يمكنها استخدام التنقل المباشر، بينما شاشة التفاصيل مع تحميل البيانات — مؤجل. التقسيم يحدد حسب بنية كل شاشة محددة.
استخدم SharedFlow بدون replay في أندرويد وcombineLatest بدون تخزين مؤقت في iOS. ألغِ الاشتراكات السابقة عند مشغل جديد. هذا يضمن معالجة آخر حدث تنقل فقط.
نعم، في Compose يتم تنفيذ التنقل المؤجل من خلال الاشتراك في StateFlow الخاص بـ ViewModel واستدعاء NavController.navigate في LaunchedEffect. توصي Google باستخدام Navigation Compose مع نموذج قائم على الأحداث للسيناريوهات المؤجلة.
أجل التنقل حتى يعود التطبيق للنشاط مرة أخرى. في أندرويد استخدم Lifecycle.State.STARTED لتصفية الأحداث. في iOS تحقق من UIApplication.State في كتل Combine أو async/await.
الملخص
سنقوم بتطوير تطبيق جوال جاهز
تقدم IT Sectr تطبيقات iOS وAndroid للشركات الناشئة والشركات منذ عام 2017. سوف نقدم لك النصح ونقترح أفضل حل.