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

المسارات المُسمَّاة

15 دقيقة الدرس 3 من 14

المسارات المُسمَّاة

مع نمو تطبيق Flutter، تصبح إدارة التنقل عبر تمرير بناة المسارات المجهولة مباشرةً إلى Navigator.push() أمرًا مُرهقًا. تحلّ المسارات المُسمَّاة هذه المشكلة بإعطاء كل شاشة معرّفًا نصيًا وتسجيل جميعها في خريطة مركزية للمسارات داخل MaterialApp. يمكن لأي ودجت في أي مكان من الشجرة الانتقال إلى شاشة بالاسم، مما يُبقي منطق التنقل مفصولًا عن التسلسل الهرمي للودجات.

ملاحظة: المسارات المُسمَّاة هي أداة التنقل المدمجة في Flutter وتعمل بدون تبعيات. وهي كافية تمامًا لمعظم التطبيقات الصغيرة والمتوسطة الحجم. بالنسبة للتطبيقات الكبيرة التي تحتاج إلى ربط عميق (deep linking) أو حراسة للمسارات أو تنقل متداخل، تعتمد حزم مثل go_router على نفس المفاهيم المُقدَّمة هنا.

تسجيل خريطة المسارات في MaterialApp

تُعرِّف كل مسار مُسمَّى مرةً واحدة داخل المعامل routes الخاص بـ MaterialApp. مفتاح الخريطة هو نص اسم المسار (بالاصطلاح يبدأ بـ /)، والقيمة هي دالة بانية تُعيد الودجت المستهدف.

تعريف المسارات المُسمَّاة في MaterialApp

void main() => runApp(const MyApp());

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  // تمركز كل اسم مسار كثابت ثابت
  // حتى يُكشف الخطأ الإملائي وقت الترجمة لا وقت التشغيل.
  static const String homeRoute    = '/';
  static const String profileRoute = '/profile';
  static const String settingsRoute = '/settings';

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Named Routes Demo',
      initialRoute: MyApp.homeRoute,
      routes: {
        MyApp.homeRoute:    (context) => const HomeScreen(),
        MyApp.profileRoute: (context) => const ProfileScreen(),
        MyApp.settingsRoute:(context) => const SettingsScreen(),
      },
    );
  }
}

يُخبر تحديد initialRoute الإطارَ أيَّ مسار مُسمَّى يعرضه أولًا. يحلّ محلّ المعامل القديم home عند استخدام خريطة routes.

التنقل باستخدام Navigator.pushNamed

بمجرد تسجيل المسارات، يصبح التنقل استدعاءً لدالة واحدة من أي ودجت. يدفع Navigator.pushNamed(context, routeName) المسار المُسمَّى على مكدس التنقل تمامًا مثل Navigator.push()، لكن دون الحاجة لاستيراد صنف الشاشة المستهدفة.

دفع المسارات المُسمَّاة واستبدالها

class HomeScreen extends StatelessWidget {
  const HomeScreen({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('الرئيسية')),
      body: Center(
        child: Column(
          mainAxisSize: MainAxisSize.min,
          children: [
            // دفع — يمكن للمستخدم العودة إلى الرئيسية
            ElevatedButton(
              onPressed: () =>
                  Navigator.pushNamed(context, MyApp.profileRoute),
              child: const Text('الذهاب إلى الملف الشخصي'),
            ),
            const SizedBox(height: 12),
            // pushReplacementNamed — يستبدل المسار الحالي (بدون زر رجوع)
            ElevatedButton(
              onPressed: () =>
                  Navigator.pushReplacementNamed(
                      context, MyApp.settingsRoute),
              child: const Text('استبدال بالإعدادات'),
            ),
            const SizedBox(height: 12),
            // pushNamedAndRemoveUntil — مسح المكدس بالكامل (مثلًا بعد تسجيل الدخول)
            ElevatedButton(
              onPressed: () =>
                  Navigator.pushNamedAndRemoveUntil(
                    context,
                    MyApp.homeRoute,
                    (route) => false, // إزالة كل المسارات السابقة
                  ),
              child: const Text('إعادة التعيين إلى الرئيسية'),
            ),
          ],
        ),
      ),
    );
  }
}
نصيحة: فضّل استخدام Navigator.pushNamed على Navigator.push في كل مكان في التطبيق. هذا يعني أن إعادة الهيكلة — إعادة تسمية شاشة أو إعادة تنظيمها — تستلزم تغيير خريطة المسارات في مكان واحد فقط، بدلًا من البحث في كل نقاط الاستدعاء.

تمرير وسيطات مُصنَّفة الأنواع عبر RouteSettings

من المتطلبات الشائعة تمرير بيانات إلى الشاشة المستهدفة — مثل معرّف مستخدم أو كائن منتج. تدعم المسارات المُسمَّاة ذلك عبر المعامل الاختياري arguments في pushNamed. تقرأ الشاشة المستقبِلة القيمة من ModalRoute.of(context)!.settings.arguments وتحوّلها إلى النوع المتوقع.

إرسال واستقبال وسيطات المسار

// --- جانب المُرسِل ---
ElevatedButton(
  onPressed: () {
    Navigator.pushNamed(
      context,
      MyApp.profileRoute,
      arguments: {'userId': 42, 'displayName': 'إدريس'},
    );
  },
  child: const Text('فتح الملف الشخصي'),
),

// --- جانب المستقبِل (ProfileScreen) ---
class ProfileScreen extends StatelessWidget {
  const ProfileScreen({super.key});

  @override
  Widget build(BuildContext context) {
    // التحويل إلى Map<String, dynamic> — استخدم صنف DTO مُسمَّى للحمولات الأكبر
    final args =
        ModalRoute.of(context)!.settings.arguments as Map<String, dynamic>;

    final int    userId      = args['userId']      as int;
    final String displayName = args['displayName'] as String;

    return Scaffold(
      appBar: AppBar(title: Text('الملف الشخصي — $displayName')),
      body: Center(
        child: Text('معرّف المستخدم: $userId'),
      ),
    );
  }
}
تحذير: سيُطلق التحويل as Map<String, dynamic> خطأً من نوع TypeError أثناء التشغيل إذا مرّر المُستدعي نوعًا خاطئًا أو تنقّل إلى المسار مباشرةً من initialRoute بدون وسيطات. احمِ بفحص القيمة الفارغة (settings.arguments as Map<String, dynamic>?) أو استخدم onGenerateRoute لتحليل وسيطات أكثر أمانًا من ناحية الأنواع.

استخدام DTO للوسيطات المُسمَّاة (أفضل ممارسة)

بالنسبة للشاشات التي تقبل عدة وسيطات، يجعل استبدال الـ Map الخام بصنف بيانات مخصص (يُسمى أحيانًا DTO وسيطات المسار) العقد بين الشاشات واضحًا ويُمسك أخطاء الأنواع وقت الترجمة.

وسيطات المسار كصنف مُصنَّف الأنواع

// تعريف الـ DTO بجانب شاشته أو في ملف منفصل
class ProfileArgs {
  final int    userId;
  final String displayName;

  const ProfileArgs({required this.userId, required this.displayName});
}

// المُستدعي
Navigator.pushNamed(
  context,
  MyApp.profileRoute,
  arguments: const ProfileArgs(userId: 42, displayName: 'إدريس'),
);

// المستقبِل
final args =
    ModalRoute.of(context)!.settings.arguments as ProfileArgs;
Text('مرحبًا، ${args.displayName}');

القيود ومتى تتجاوز هذا النهج

  • لا أمان لأسماء المسارات وقت الترجمة — أسماء المسارات لا تزال نصوصًا عاديةً؛ اسم مُخطأ يُسبب خطأً وقت التشغيل. استخدم الثوابت الساكنة (كما هو موضح أعلاه) للتخفيف من ذلك.
  • لا مزامنة مع عنوان URL على الويب — لا تُحدِّث المسارات المُسمَّاة شريط URL المتصفح بطريقة صديقة لمحركات البحث. استخدم go_router للأهداف على الويب.
  • لا حراسة للتنقل — لا توجد طريقة مدمجة لحجب مسار (مثلًا توجيه المستخدمين غير المصادَق عليهم). استخدم onGenerateRoute للحراسة الأمرية أو redirect في go_router للحراسة التصريحية.

ملخص

تُمركز المسارات المُسمَّاة خريطة التنقل في تطبيقك في MaterialApp.routes، مما يتيح لأي ودجت التنقل بالاسم عبر Navigator.pushNamed. تُمرَّر الوسيطات عبر pushNamed(arguments: ...) وتُسترجع من ModalRoute.of(context)!.settings.arguments. استخدام DTOs مُصنَّفة الأنواع بدلًا من الخرائط الخام يجعل العقد بين المُستدعي والشاشة واضحًا وآمنًا. بالنسبة للتطبيقات التي تحتاج إلى روابط عميقة أو مزامنة URL مع الويب أو حراسة للمصادقة، تعتمد هذه الأساسيات مباشرةً حزمَ مثل go_router.