التنقل والتوجيه

انتقالات التنقل وأفضل الممارسات

16 دقيقة الدرس 14 من 14

انتقالات التنقل وأفضل الممارسات

التطبيق المصقول في Flutter لا يكون صحيحاً فحسب — بل يُحسّ بأنه صحيح. الانتقالات المخصصة بين الصفحات، وتنظيم تعريفات المسارات، والتعامل المتسق مع زر الرجوع، كلها تفرق بين تطبيق يصفه المستخدمون بأنه "سلس" وآخر يصفونه بأنه "متقطع". في هذا الدرس ستتقن CustomTransitionPage، وتتعلم كيفية هيكلة المسارات لقواعد الكود الكبيرة، وتطبق أفضل ممارسات التنقل التي تمنع الأخطاء الشائعة في وقت التشغيل.

فهم CustomTransitionPage

يلف GoRouter كل وجهة في كائن صفحة. بشكل افتراضي يستخدم MaterialPage (انزلاق بأسلوب Android) أو CupertinoPage (انزلاق بأسلوب iOS). لاستبدال الرسوم المتحركة بالكامل، قدّم CustomTransitionPage كـ pageBuilder لـ GoRoute.

انتقال تلاشي مع CustomTransitionPage

GoRoute(
  path: '/details/:id',
  pageBuilder: (context, state) {
    return CustomTransitionPage<void>(
      key: state.pageKey,
      child: DetailsPage(id: state.pathParameters['id']!),
      transitionDuration: const Duration(milliseconds: 350),
      transitionsBuilder: (context, animation, secondaryAnimation, child) {
        // تلاشي للداخل عند الدفع، تلاشي للخارج عند الإزالة
        return FadeTransition(
          opacity: CurveTween(curve: Curves.easeInOut).animate(animation),
          child: child,
        );
      },
    );
  },
),

معاملات المُنشئ الرئيسية:

  • key — مرر دائماً state.pageKey حتى يستطيع GoRouter التمييز بين الصفحات أثناء الانتقالات.
  • child — ودجت الوجهة (مبني مسبقاً؛ لا يوجد استدعاء builder هنا).
  • transitionDuration — يتحكم في مدة الدفع؛ reverseTransitionDuration يتحكم في مدة الإزالة بشكل منفصل.
  • transitionsBuilder — يستقبل أربعة معاملات: context، وanimation (من 0 إلى 1 عند الدفع)، وsecondaryAnimation (من 0 إلى 1 عندما تُدفع صفحة جديدة فوقها)، وchild.
نصيحة: اجمع الرسوم المتحركة بلف child في عدة ودجات انتقال. التركيب الشائع هو FadeTransition حول SlideTransition لإنتاج تأثير تلاشي وانزلاق دون أي حزم إضافية.

أمثلة على انتقالي الانزلاق والتدرج

نمطان آخران ستواجههما في تطبيقات الإنتاج:

انتقالا الانزلاق للأعلى والتدرج

// انزلاق للأعلى من الأسفل (أسلوب نافذة منبثقة)
transitionsBuilder: (context, animation, secondaryAnimation, child) {
  final tween = Tween(
    begin: const Offset(0.0, 1.0),
    end: Offset.zero,
  ).chain(CurveTween(curve: Curves.easeOutCubic));
  return SlideTransition(position: animation.drive(tween), child: child);
},

// تدرج من المركز (تأثير بقعة الضوء)
transitionsBuilder: (context, animation, secondaryAnimation, child) {
  return ScaleTransition(
    scale: Tween<double>(begin: 0.85, end: 1.0)
        .chain(CurveTween(curve: Curves.easeOut))
        .animate(animation),
    child: FadeTransition(
      opacity: animation,
      child: child,
    ),
  );
},
ملاحظة: احرص على أن تكون مدد الانتقال بين 200 مللي ثانية و400 مللي ثانية. ما هو أسرع من 200 مللي ثانية يبدو مفاجئاً، وما هو أبطأ من 400 مللي ثانية يبدو بطيئاً على الأجهزة المنخفضة الأداء. تنصح إرشادات Material Design بـ 300 مللي ثانية لمعظم انتقالات الشاشة.

تنظيم المسارات للتطبيقات الكبيرة

وضع كل GoRoute في ملف واحد يصبح صعب الإدارة بمجرد أن ينمو التطبيق إلى أكثر من اثنتي عشرة شاشة. النمط الموصى به هو تقسيم تعريفات المسارات حسب الميزة:

  • أنشئ مجلد router/ تحت lib/.
  • كل ميزة تُظهر ثابت List<RouteBase> (مثل authRoutes، shopRoutes).
  • ملف app_router.dart على المستوى الأعلى يدمجها في نسخة GoRouter واحدة.
  • استخدم المسارات المسماة (المعامل name: في GoRoute) وتنقل باستخدام context.goNamed('routeName') حتى لا تكسر نقاط الاستدعاء عند تغيير المسارات.
نصيحة: عرّف ثوابت أسماء المسارات في فئة AppRoutes مجردة واحدة بحقول const String ساكنة. هذا يزيل السلاسل السحرية من كل استدعاء تنقل ويجعل إعادة تسمية مسار تعديلاً من سطر واحد.

معالجة الأخطاء في التنقل

يوفر GoRouter خطافَين لأخطاء التنقل:

  • errorBuilder — يعرض ودجت عندما لا يطابق أي مسار المسار المطلوب (على غرار خطأ 404).
  • redirect — يعترض كل حدث تنقل؛ ألقِ GoException أو أعد مسار التحويل لحماية المسارات (حراسة المصادقة، علامات الميزات).
تحذير: لا تستدعِ context.go() أو Navigator.push() داخل طريقة build() للودجت. يجب أن يكون التنقل مُشغَّلاً بواسطة أحداث المستخدم (callbacks) أو callbacks ما بعد الإطار (WidgetsBinding.instance.addPostFrameCallback). التنقل أثناء البناء يسبب خطأ "setState() called during build" ويمكن أن يعطّل التطبيق.

سلوك زر الرجوع في Android

يتكامل GoRouter مع زر الرجوع في Android تلقائياً عبر ودجت Router. ومع ذلك توجد حالات يجب فيها التعامل معه صراحةً:

  • لف الشاشة بـ PopScope (Flutter 3.16+؛ يحل محل WillPopScope المهمل) لاعتراض ضغطات الرجوع.
  • اضبط canPop: false لمنع الإزالة التلقائية (مثلاً شاشات تأكيد الدفع).
  • استخدم callback يُدعى onPopInvokedWithResult لعرض مربع حوار "هل تريد الخروج؟" قبل السماح بالإزالة.
  • على الويب وسطح المكتب، تذكر أن زر الرجوع في المتصفح/نظام التشغيل يطلق نفس إزالة الراوتر — اختبر على جميع المنصات.

ملخص أفضل الممارسات

  • مرر دائماً state.pageKey إلى CustomTransitionPage لتجنب أخطاء المفاتيح.
  • استخدم المسارات المسماة وفئة ثوابت AppRoutes مركزية.
  • قسّم أشجار المسارات الكبيرة إلى قوائم لكل ميزة وادمجها على مستوى التطبيق.
  • قدّم errorBuilder حتى لا تُظهر المسارات غير المتطابقة شاشة فارغة أبداً.
  • استخدم PopScope (وليس WillPopScope) لاعتراض زر الرجوع.
  • احتفظ بمدد الانتقال بين 200–400 مللي ثانية.
  • لا تتنقل داخل build()؛ استخدم callbacks أو addPostFrameCallback.
الخلاصة الرئيسية: الانتقالات المدروسة وكود المسارات المنظم يعملان معاً. تمنحك CustomTransitionPage تحكماً على مستوى البكسل في كيفية ظهور الشاشات واختفائها، بينما تُبقي هياكل المسارات المقسمة حسب الميزة والمسارات المسماة رسماً بيانياً كبيراً للتنقل قابلاً للصيانة وآمناً عند إعادة الهيكلة.