شرح التنقل بين الصفحات في Flutter

شرح التنقل بين الصفحات في Flutter

التنقل بين الصفحات في Flutter من أكثر الأشياء التي تُشعرك سريعًا بأنك تبني تطبيقًا حقيقيًا، لا مجرد شاشات متفرقة. الفكرة بسيطة في ظاهرها، لكنها مهمة جدًا في تجربة المستخدم: كيف ينتقل من شاشة إلى أخرى بسلاسة، كيف يعود للخلف، كيف نمرر البيانات بين الصفحات، وكيف نبني تدفقًا مرتبًا لا يتحول مع الوقت إلى فوضى. في Flutter، تُسمّى الشاشة أو الصفحة Route، وNavigator هو المسؤول عن إدارة هذا التاريخ الكامل من الصفحات على شكل مكدس، أي أن كل صفحة جديدة تُضاف فوق الصفحة السابقة، ثم يمكن إزالتها والعودة للخلف عند الحاجة. كما توضح الوثائق الرسمية، Flutter تدعم التنقل بأسلوب إمّا إمبريالي عبر Navigator و push() و pop()، أو بأسلوب Declarative عبر Router وحلول مثل go_router، ومع أن المسارات المسماة مفيدة في بعض الحالات البسيطة، فإن Flutter نفسها لا توصي بها لمعظم التطبيقات الحديثة.

حين تبدأ في بناء تطبيق جديد، من السهل أن تتعامل مع التنقل كأنه مجرد زر يفتح شاشة أخرى، لكن الحقيقة أوسع من ذلك بكثير. التنقل هو جزء من هيكل التطبيق نفسه، لأنه يحدد كيف يتنقل المستخدم بين أقسامه، وكيف تُحفظ حالة الرجوع، وكيف تتصرف الروابط العميقة على الويب أو على الهاتف، وكيف تنظم الشاشات التي تعمل داخل تدفق خاص بها مثل صفحة التسجيل أو الإعداد الأولي. ولهذا السبب من المهم أن تفهم الفرق بين التنقل البسيط الذي يعتمد على Navigator.push() و Navigator.pop()، وبين التنقل الأكثر تنظيمًا الذي يعتمد على المسارات المسماة أو على Router packages. Flutter توضح أيضًا أن Navigator و Router صُمّما ليعملا معًا، وأن بعض الحالات تحتاج إلى مسارات page-backed بينما بعض الحالات تنتج pageless routes مثل showDialog، وهذا ينعكس مباشرة على سلوك التتبع والروابط العميقة.

إذا أردت أن تفهم التنقل في Flutter من جذوره، فابدأ بصورة المكدس. تخيل أن لديك شاشة رئيسية، ثم ضغط المستخدم على بطاقة أو زر، فانتقلت إلى شاشة التفاصيل. هنا لا تختفي الشاشة الأولى، بل تبقى في أسفل المكدس، وتُضاف شاشة التفاصيل فوقها. عندما ينفّذ المستخدم الرجوع، يتم حذف الشاشة العليا ويعود التطبيق إلى الشاشة السابقة. هذا هو السبب في أن Navigator.push() يدفع Route جديدة إلى المكدس، بينما Navigator.pop() يزيل الصفحة الحالية من أعلى المكدس. هذه الفكرة موجودة بوضوح في دليل Flutter الرسمي الخاص بـ “Navigate to a new screen and back”، والذي يشرح أن الصفحة في Flutter هي widget، وأن Navigator هو الأداة التي تدير عملية الإضافة والإزالة من المكدس.

لنبدأ بأبسط مثال عملي، وهو الانتقال من شاشة إلى شاشة أخرى ثم الرجوع. هذا المثال مناسب جدًا لفهم الأساس قبل أن ندخل في المسارات المسماة أو تمرير البيانات. لاحظ أن MaterialPageRoute يُستخدم كثيرًا لأنه يمنحك انتقالًا مناسبًا للمنصة ويجعل إنشاء الصفحة الجديدة مباشرًا وواضحًا. هذا الأسلوب، مع Navigator، هو أحد أكثر الأساليب المباشرة التي توصي بها Flutter في كثير من الحالات البسيطة بدلًا من المبالغة في التعقيد مبكرًا.

import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

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

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
      debugShowCheckedModeBanner: false,
      home: FirstScreen(),
    );
  }
}

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('الشاشة الأولى')),
      body: Center(
        child: ElevatedButton(
          onPressed: () {
            Navigator.push(
              context,
              MaterialPageRoute(
                builder: (_) => const SecondScreen(),
              ),
            );
          },
          child: const Text('انتقل إلى الشاشة الثانية'),
        ),
      ),
    );
  }
}

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('الشاشة الثانية')),
      body: Center(
        child: ElevatedButton(
          onPressed: () {
            Navigator.pop(context);
          },
          child: const Text('رجوع'),
        ),
      ),
    );
  }
}

في هذا المثال، كل شيء يدور حول سطرين تقريبًا: Navigator.push() للانتقال، و Navigator.pop() للعودة. قد يبدو هذا بسيطًا جدًا، لكنه يكفي لفهم 70% من حالات التنقل اليومية في التطبيقات الصغيرة والمتوسطة. الجميل في هذا الأسلوب أنه واضح، مقروء، ويسهل تتبعه أثناء التطوير والتصحيح. Flutter تشرح أن push() يضيف Route جديدة إلى المكدس، بينما pop() يزيل Route الحالية، وهذا يجعل تجربة الرجوع متوقعة جدًا بالنسبة للمستخدم.

لكن التطبيقات لا تعيش طويلاً على التنقل البسيط فقط. في كثير من الحالات ستحتاج إلى فتح نفس الشاشة من أماكن مختلفة، أو جعل الشاشات قابلة للوصول باسم ثابت بدلًا من إنشاء MaterialPageRoute في كل مرة. هنا تأتي فكرة named routes. في Flutter يمكنك تعريف أسماء للمسارات داخل MaterialApp.routes ثم التنقل باستخدام Navigator.pushNamed(). هذا الأسلوب ما يزال مدعومًا ومفيدًا، خاصة في التطبيقات الصغيرة أو البنى البسيطة، لكنه ليس الخيار المفضل لمعظم التطبيقات الحديثة لأن Flutter توضح أن Named Routes تملك قيودًا في deep linking وسلوك التصفح، لذلك تُفضل Flutter في الغالب go_router أو Navigator مع MaterialPageRoute حسب الحالة.

إليك مثالًا واضحًا على المسارات المسماة، وهو مثال ممتاز عندما تريد أن يكون اسم الصفحة معروفًا وثابتًا في أكثر من مكان داخل المشروع. الفائدة هنا ليست فقط تقليل التكرار، بل أيضًا جعل الانتقال أكثر تنظيمًا عندما يكبر المشروع قليلًا. ومع ذلك، من المفيد أن تتذكر أن Flutter تذكر صراحة أن Named Routes ليست الخيار الأفضل لمعظم التطبيقات، لذلك تعامل معها كأداة مفيدة لا كقاعدة ذهبية.

import 'package:flutter/material.dart';

void main() {
  runApp(const NamedRoutesApp());
}

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      initialRoute: '/',
      routes: {
        '/': (context) => const HomeScreen(),
        '/details': (context) => const DetailsScreen(),
      },
    );
  }
}

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('الرئيسية')),
      body: Center(
        child: ElevatedButton(
          onPressed: () {
            Navigator.pushNamed(context, '/details');
          },
          child: const Text('فتح شاشة التفاصيل'),
        ),
      ),
    );
  }
}

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('التفاصيل')),
      body: Center(
        child: ElevatedButton(
          onPressed: () {
            Navigator.pop(context);
          },
          child: const Text('رجوع'),
        ),
      ),
    );
  }
}

من الأشياء التي يواجهها المطور مبكرًا أيضًا: كيف أرسل بيانات إلى الصفحة الجديدة؟ هذه نقطة مهمة جدًا لأن التنقل ليس فقط انتقالًا بصريًا، بل هو انتقال للسياق أيضًا. قد يكون لديك اسم مستخدم، أو كائن منتج، أو معرّف طلب، أو حتى نص بسيط تريد عرضه في صفحة أخرى. Flutter توفر أكثر من طريقة، وأكثر طريقة عملية في التنقل التقليدي هي تمرير البيانات عبر constructor عند استخدام MaterialPageRoute، أو عبر arguments عند استخدام المسارات المسماة. الوثائق الرسمية تشرح بوضوح أن تمرير البيانات إلى شاشة جديدة ممكن باستخدام Navigator.push() مع RouteSettings, أو عبر Navigator.pushNamed() مع arguments، ثم استرجاع البيانات داخل الشاشة المستهدفة.

إليك المثال الكلاسيكي لتمرير كائن إلى شاشة التفاصيل. هذا الأسلوب غالبًا هو الأوضح، لأنك تضع البيانات التي تحتاجها الصفحة داخل constructor مباشر، فيصبح الكود أكثر صراحة وأسهل للصيانة. في التطبيقات الواقعية، هذا الأسلوب ممتاز عندما تكون الشاشات مستقلة نسبيًا وتعتمد على بيانات محددة يجب أن تصلها بوضوح.

import 'package:flutter/material.dart';

class Product {
  final String title;
  final String description;

  const Product({
    required this.title,
    required this.description,
  });
}

void main() {
  runApp(const DataPassingApp());
}

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: ProductsScreen(),
    );
  }
}

class ProductsScreen extends StatelessWidget {
  ProductsScreen({super.key});

  final List<Product> products = const [
    Product(title: 'لابتوب', description: 'جهاز قوي للعمل والتطوير'),
    Product(title: 'هاتف', description: 'مناسب للاستخدام اليومي'),
    Product(title: 'سماعات', description: 'صوت واضح ومريح'),
  ];

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('المنتجات')),
      body: ListView.builder(
        itemCount: products.length,
        itemBuilder: (context, index) {
          final product = products[index];
          return ListTile(
            title: Text(product.title),
            subtitle: Text(product.description),
            onTap: () {
              Navigator.push(
                context,
                MaterialPageRoute(
                  builder: (_) => ProductDetailsScreen(product: product),
                ),
              );
            },
          );
        },
      ),
    );
  }
}

class ProductDetailsScreen extends StatelessWidget {
  final Product product;

  const ProductDetailsScreen({super.key, required this.product});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text(product.title)),
      body: Padding(
        padding: const EdgeInsets.all(16),
        child: Text(product.description),
      ),
    );
  }
}

وأحيانًا لا تحتاج فقط إلى إرسال بيانات إلى الصفحة الجديدة، بل تحتاج أيضًا إلى إرجاع نتيجة عند الرجوع. هذا السيناريو شائع جدًا، مثل اختيار عنصر من قائمة، أو تحديد لون، أو اختيار بلد، أو تأكيد عملية معينة. Flutter تدعم ذلك بشكل مباشر عبر Navigator.pop(context, result)، وتقول الوثائق إن pop() يمكن أن يأخذ نتيجة اختيارية يتم إرجاعها إلى الـ Future الذي ينتظر في الشاشة السابقة. هذا يجعل التفاعل بين الصفحات أكثر قوة وأناقة من مجرد الرجوع الصامت.

هذا المثال يوضح كيف تُعيد قيمة من صفحة إلى أخرى، ثم تتعامل معها في الصفحة الأصلية. لاحظ أن الصفحة الأولى تنتظر نتيجة من Navigator.push() لأن هذا الاستدعاء يعيد Future. وعندما تنتهي الصفحة الثانية، يتم تمرير القيمة المسترجعة إلى الصفحة الأولى. هذه الفكرة مهمة جدًا في أي تطبيق يتضمن اختيارًا أو تأكيدًا أو حوارًا بين المستخدم والتطبيق.

import 'package:flutter/material.dart';

void main() {
  runApp(const ReturnResultApp());
}

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: const HomePage(),
    );
  }
}

class HomePage extends StatefulWidget {
  const HomePage({super.key});

  @override
  State<HomePage> createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> {
  String selectedValue = 'لم يتم الاختيار بعد';

  Future<void> openSelectionPage() async {
    final result = await Navigator.push<String>(
      context,
      MaterialPageRoute(builder: (_) => const SelectionPage()),
    );

    if (result != null) {
      setState(() {
        selectedValue = result;
      });
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('الصفحة الرئيسية')),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Text(selectedValue),
            const SizedBox(height: 16),
            ElevatedButton(
              onPressed: openSelectionPage,
              child: const Text('اختر قيمة'),
            ),
          ],
        ),
      ),
    );
  }
}

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('صفحة الاختيار')),
      body: Center(
        child: ElevatedButton(
          onPressed: () {
            Navigator.pop(context, 'تم اختيار القيمة بنجاح');
          },
          child: const Text('إرجاع النتيجة'),
        ),
      ),
    );
  }
}

هناك أيضًا حالات لا يكفي فيها مجرد فتح صفحة جديدة فوق الصفحة الحالية، بل تريد استبدال الصفحة الحالية بالكامل، أو حتى إزالة أكثر من صفحة من المكدس. هنا تظهر الدوال مثل pushReplacement() و pushAndRemoveUntil(). هذه الأدوات مفيدة جدًا عندما يكون عندك تدفق لا تريد للمستخدم أن يعود منه إلى الخطوات السابقة، مثل شاشة تسجيل الدخول بعد نجاح المصادقة، أو شاشة splash ثم الانتقال إلى الصفحة الرئيسية مع حذف صفحة البداية من المكدس. بهذه الطريقة تمنع الرجوع إلى صفحات لم يعد من المنطقي أن تبقى في الذاكرة الظاهرة للمستخدم.

pushReplacement() يستبدل الصفحة الحالية بصفحة جديدة، وهو حل مناسب عندما تريد أن يمضي المستخدم قدمًا دون أن يعود للشاشة السابقة عبر زر الرجوع. أما pushAndRemoveUntil() فهو أقوى، لأنه يتيح لك الاحتفاظ بالصفحات التي تحددها فقط، أو حذف كل شيء تقريبًا حسب الشرط الذي تكتبه. في التطبيقات العملية، هذا يُستخدم كثيرًا بعد تسجيل الدخول، أو بعد إتمام عملية حجز، أو عند إنهاء flow معين. الفكرة كلها أن مكدس Navigator ليس مجرد سجل عشوائي، بل أداة يمكنك التحكم بها لتصميم تجربة مستخدم منطقية.

Navigator.pushReplacement(
  context,
  MaterialPageRoute(builder: (_) => const HomeScreen()),
);

Navigator.pushAndRemoveUntil(
  context,
  MaterialPageRoute(builder: (_) => const HomeScreen()),
  (route) => false,
);

من النقاط المهمة جدًا أيضًا فهم التنقل المتداخل أو Nested Navigation. قد تتخيل أن كل التطبيق يحتاج Navigator واحد فقط، لكن الواقع أن بعض التدفقات تعمل بشكل أفضل عندما تمتلك Navigator خاصًا بها داخل جزء من الواجهة. Flutter تعرض مثالًا رسميًا لتدفق إعداد متعدد الصفحات يعمل تحت Navigator أعلى التطبيق، بحيث يملك هذا التدفق التحكم المحلي في تنقله الخاص بدلًا من مزاحمة التنقل العام للتطبيق. هذا النمط مفيد جدًا في معالجات الإعداد، والـ onboarding الطويل، وشاشات الإعدادات متعددة المراحل، وحتى بعض البنى التي تتضمن tabs مع history مستقل لكل تبويب.

الفائدة الحقيقية من Navigator المتداخل أنه يحافظ على العزلة بين الأقسام. بدلًا من أن تضطر كل خطوة في تدفق معين إلى التلاعب بالمكدس الرئيسي للتطبيق، يمكنك أن تمنح ذلك التدفق مكدسًا داخليًا يديره بنفسه. هذا يجعل الكود أسهل في التفكير، ويقلل من التعارضات، ويمنع أحيانًا الأخطاء التي تظهر عندما تُخلط صفحات التهيئة مع الصفحات الأساسية في نفس التاريخ التنقلي. Flutter نفسها تشير إلى أن هذا النوع من التفويض يكون غالبًا أفضل عند بناء software حقيقي فيه أكثر من مسار مستخدم واحد.

import 'package:flutter/material.dart';

void main() {
  runApp(const NestedNavApp());
}

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: const ShellPage(),
    );
  }
}

class ShellPage extends StatefulWidget {
  const ShellPage({super.key});

  @override
  State<ShellPage> createState() => _ShellPageState();
}

class _ShellPageState extends State<ShellPage> {
  int index = 0;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: IndexedStack(
        index: index,
        children: const [
          HomeTab(),
          SettingsFlow(),
        ],
      ),
      bottomNavigationBar: BottomNavigationBar(
        currentIndex: index,
        onTap: (value) => setState(() => index = value),
        items: const [
          BottomNavigationBarItem(icon: Icon(Icons.home), label: 'الرئيسية'),
          BottomNavigationBarItem(icon: Icon(Icons.settings), label: 'الإعدادات'),
        ],
      ),
    );
  }
}

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

  @override
  Widget build(BuildContext context) {
    return const Center(child: Text('محتوى الرئيسية'));
  }
}

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

  @override
  Widget build(BuildContext context) {
    return Navigator(
      onGenerateRoute: (_) {
        return MaterialPageRoute(
          builder: (_) => const SettingsStepOne(),
        );
      },
    );
  }
}

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('الإعدادات - الخطوة 1')),
      body: Center(
        child: ElevatedButton(
          onPressed: () {
            Navigator.of(context).push(
              MaterialPageRoute(builder: (_) => const SettingsStepTwo()),
            );
          },
          child: const Text('الخطوة التالية'),
        ),
      ),
    );
  }
}

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('الإعدادات - الخطوة 2')),
      body: const Center(child: Text('هنا خطوة أخرى داخل Navigator متداخل')),
    );
  }
}

وفي التطبيقات التي تعتمد على الويب أو الروابط العميقة أو البنية الكبيرة، تبدأ أهمية Router وgo_router بالظهور أكثر. Flutter توضح أن التطبيقات ذات متطلبات التنقل المتقدمة، مثل التطبيقات التي تحتاج روابط مباشرة إلى كل شاشة أو التي تستخدم عدة Navigator widgets، يُفضّل لها استخدام package routing مثل go_router الذي يحلل مسار الرابط ويعيد تكوين Navigator عند استقبال deep link جديد. كما تشير صفحة recommendations الرسمية إلى أن go_router هو الخيار المفضل لجزء كبير جدًا من تطبيقات Flutter، مع بقاء Navigator مباشرًا مناسبًا في الحالات التي لا تحتاج هذا المستوى من التعقيد.

هذا لا يعني أن كل مشروع يجب أن يبدأ بـ go_router من أول سطر، ولا يعني أن Navigator التقليدي قد انتهى. المعنى الحقيقي هو أن الأداة يجب أن تناسب حجم التطبيق وطبيعته. إذا كان تطبيقك بسيطًا، أو شاشاته قليلة، أو لا يعتمد على روابط عميقة معقدة، فقد يكون Navigator مباشرة مع MaterialPageRoute هو الخيار الأسهل والأوضح. أما إذا كان التطبيق يحتوي على web URLs، أو tabs مع deep linking، أو flow متعدد الطبقات، فحينها يصبح go_router عادةً أكثر راحة وتنظيمًا، وهو ما توضحه Flutter في وثائقها الرسمية.

في Flutter web تحديدًا، يصبح التنقل مرتبطًا أيضًا بطريقة صياغة الرابط نفسه. Flutter تشرح أن تطبيقات الويب تدعم استراتيجيتين لعنوان URL: hash strategy وpath strategy. هذا قد لا يظهر للمستخدم العادي كفارق كبير، لكنه مهم جدًا للمطور الذي يريد روابط قابلة للمشاركة أو الفهرسة أو الفهم الواضح في المتصفح. ومع التنقل المعتمد على Router أو مكتبات مثل go_router، يصبح التحكم في هذه التفاصيل أكثر اتساعًا ومرونة من الأسلوب التقليدي القائم على المسارات المسماة فقط.

أحد الأخطاء الشائعة لدى المبتدئين هو استخدام named routes في كل مكان لمجرد أنها تبدو “مرتبة”. التنظيم مهم، لكنه ليس مبررًا كافيًا إذا صار الكود أصعب في التتبع أو ضعفت القدرة على تمرير البيانات أو التخصيص. Flutter تذكر بوضوح أن المسارات المسماة ليست موصى بها لمعظم التطبيقات، وأن بعض احتياجات deep linking لا تُلبى بأفضل صورة عبرها. لذلك، من الأفضل أن تختار أسلوب التنقل بناءً على الحاجة الحقيقية، لا على الإحساس الأولي بأن الأسماء تبدو أنظف. أحيانًا يكون الـ constructor المباشر أو MaterialPageRoute أبسط وأقوى وأكثر قابلية للصيانة من جدول routes ضخم.

ومن الجوانب اللطيفة في Flutter أن كل شيء تقريبًا يدور حول widgets، حتى الصفحة نفسها ليست كيانًا بعيدًا أو معقدًا. هذا يجعل بناء التنقل أقرب إلى بناء واجهة طبيعية: أنت لا “تفتح نافذة” بالمعنى التقليدي، بل تدفع widget جديدة إلى الواجهة، ثم تزيلها عندما تنتهي الحاجة إليها. هذه النظرة تساعدك على التفكير بطريقة صحيحة: لا تتعامل مع الصفحات كملفات HTML ثابتة، بل كبناءات قابلة للتركيب، وتعيش داخل شجرة widget أكبر. ولهذا السبب يظل فهم BuildContext وNavigator.of(context) مهمًا جدًا في أي مشروع Flutter جاد.

TextButton(
  onPressed: () {
    Navigator.of(context).pop();
  },
  child: const Text('رجوع'),
)

إذا كنت تبني صفحة تحتوي على قائمة طويلة، ففكر دائمًا في تجربة المستخدم مع التنقل قبل أن تفكر فقط في الشكل. عندما يضغط المستخدم على عنصر من القائمة، هل من الأفضل فتح شاشة تفاصيل كاملة؟ أم Bottom Sheet؟ أم Dialog؟ أم انتقال يملأ الشاشة؟ Flutter تتيح هذه الخيارات كلها، لكن الاختيار الجيد يعتمد على طبيعة المهمة. إن كانت المعلومة مهمة ومفصلة، فصفحة جديدة غالبًا أفضل. إن كانت العملية سريعة ومؤقتة، فربما showDialog أو showModalBottomSheet يكون أوضح. Flutter تصف بعض هذه العناصر بأنها pageless routes، وهي لا تعمل تمامًا مثل الصفحات الكاملة، وهذا فرق مهم في فهمك للتنقل داخل التطبيق.

المناسب في النهاية هو أن تجعل التنقل جزءًا من منطق التطبيق، لا مجرد استجابة سطحية للنقر. فالمسار الجيد هو الذي يحترم حالة المستخدم، ويحافظ على تدفقه، ويجعله يفهم أين هو الآن وكيف يعود وأين سيصل بعد ذلك. وعندما تبني تطبيقًا وتراجع التنقل فيه، اسأل نفسك: هل هذا المسار منطقي؟ هل أحتاج فعلاً إلى صفحة جديدة أم يكفي تحديث الواجهة الحالية؟ هل يجب أن أحتفظ بصفحة في المكدس أم أستبدلها؟ هذه الأسئلة الصغيرة هي التي تفرّق بين تطبيق عادي وتطبيق مريح فعلًا في الاستخدام.

ولأن المقال موجه أيضًا لمن يريد كتابة كود نظيف، فهذه مجموعة نصائح عملية تنفعك في أي مشروع Flutter تقريبًا: اجعل أسماء الصفحات واضحة، ومرر البيانات بشكل صريح، ولا تُكثر من التلاعب بالمكدس دون سبب، واستخدم التنقل المتداخل عندما يكون تدفق معين مستقلًا بذاته، ولا تتردد في استخدام Router أو go_router عندما يكبر التطبيق أو يحتاج إلى روابط عميقة وتصفح أكثر تنظيمًا. هذه ليست قواعد جامدة، لكنها خلاصة منطقية لما توضحه وثائق Flutter الرسمية حول التنقل والر routing.

// مثال مختصر لفكرة تمرير نتيجة
final result = await Navigator.push<String>(
  context,
  MaterialPageRoute(builder: (_) => const SelectionPage()),
);

if (result != null) {
  print('النتيجة: $result');
}

قد يبدو كل ما سبق تقنيًا جدًا، لكن التنقل في Flutter يحمل جانبًا إنسانيًا أيضًا. المستخدم لا يفكر في Navigator ولا في Route ولا في deep linking، بل يفكر في شيء أبسط بكثير: هل التطبيق يفهمني؟ هل أستطيع الرجوع بسهولة؟ هل ما أراه الآن منطقي بعد الخطوة السابقة؟ كل سطر تكتبه في التنقل يؤثر في هذا الشعور. ولهذا السبب، عندما تنجح في بناء تدفق واضح ومريح، فإنك لا تكتب فقط كودًا صحيحًا، بل تصنع تجربة استخدام هادئة ومحترمة. هذا هو جوهر التنقل الجيد في Flutter: أن يبدو طبيعيًا جدًا لدرجة أن المستخدم لا يلاحظ تعقيده أصلًا.

وفي المشاريع الحقيقية، ستجد نفسك تمزج أكثر من أسلوب واحد. قد تستخدم Navigator.push() للشاشات البسيطة، و pushReplacement() لشاشة تسجيل الدخول، و pushNamed() في جزء صغير من التطبيق، وgo_router في البنية الأوسع التي تحتاج روابط عميقة، و Navigator متداخلًا داخل flow مستقل. هذا المزيج ليس تناقضًا، بل هو علامة على أنك اخترت كل أداة في المكان الذي يناسبها. Flutter نفسها لا تدفعك إلى أسلوب واحد قسريًا؛ بل تعطيك Navigator التقليدي، وتفتح الباب لأسلوب Router، وتوصي بحلول مثل go_router عندما يتطلب التطبيق ذلك.

في النهاية، تعلم التنقل بين الصفحات في Flutter ليس مجرد حفظ أوامر. هو فهم لفكرة الحركة داخل التطبيق: كيف تبدأ، كيف تنتقل، كيف تعود، كيف تُسلّم البيانات، وكيف تحافظ على الترتيب الداخلي للتجربة. إذا أتقنت هذا الجزء، ستشعر أن بناء التطبيقات أصبح أكثر سلاسة، لأن كل شاشة ستعرف مكانها الطبيعي داخل رحلة المستخدم. وخذها قاعدة بسيطة: ابدأ بالأبسط، افهم المكدس جيدًا، ثم توسع تدريجيًا إلى named routes، ثم إلى التنقل المتداخل، ثم إلى Router وgo_router عندما يصبح التطبيق كبيرًا بما يكفي ليحتاج ذلك. هذا التسلسل وحده يوفر عليك كثيرًا من التشتت لاحقًا.

#شرح التنقل بين الصفحات في Flutter #Navigator #Navigator push #Navigator pop #named routes #go_router #تمرير البيانات بين الصفحات #إرجاع البيانات من صفحة #Flutter navigation #التنقل المتداخل #Flutter routes #شرح Flutter بالعربي