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

حراسة المسارات وإعادة التوجيه في GoRouter

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

حراسة المسارات وإعادة التوجيه في GoRouter

تُعدّ حماية المسارات المعتمدة على المصادقة من أهم المتطلبات في أي تطبيق Flutter واقعي. يعالج GoRouter هذا بأناقة من خلال دالة الاستدعاء redirect والمعامل refreshListenable. يسمحان معاً باعتراض كل محاولة تنقل، وتقييم ما إذا كان المستخدم مُخوَّلاً بالمتابعة، وإعادة توجيهه تلقائياً إلى شاشة تسجيل الدخول إن لم يكن كذلك — وكل هذا بشكل تفاعلي تماماً دون كتابة كود تنقل نمطي في كل شاشة.

ملاحظة: حراسة المسارات في GoRouter تصريحية. بدلاً من تفريق فحوصات المصادقة داخل الشاشات الفردية، تمركز المنطق في مكان واحد: دالة الاستدعاء redirect في نسخة GoRouter. يجعل هذا شاشاتك نظيفة ويُيسّر مراجعة الحماية.

كيف تعمل دالة الاستدعاء redirect

دالة الاستدعاء redirect هي دالة اختيارية تُزوِّدها لـ GoRouter. تُستدعى قبل إتمام أي تنقل. تستقبل BuildContext وGoRouterState يصفان الوجهة المقصودة. يمكنك فحص الحالة، والقرار بالسماح بالتنقل، وإعادة إما null (للسماح) أو سلسلة مسار جديدة (لإعادة التوجيه). تُطلَق عند كل حدث تنقل، بما في ذلك الرابط العميق الأولي.

دالة الاستدعاء redirect الأساسية

GoRouter(
  initialLocation: '/home',
  redirect: (BuildContext context, GoRouterState state) {
    final bool isLoggedIn = AuthService.instance.isAuthenticated;
    final bool isGoingToLogin = state.matchedLocation == '/login';

    // غير مسجل دخول وليس متجهاً إلى تسجيل الدخول؟ أعِد التوجيه.
    if (!isLoggedIn && !isGoingToLogin) return '/login';

    // مسجل دخول لكن متجه إلى تسجيل الدخول؟ أرسله إلى الرئيسية.
    if (isLoggedIn && isGoingToLogin) return '/home';

    // اسمح بالتنقل كما طُلب.
    return null;
  },
  routes: [
    GoRoute(path: '/login',  builder: (ctx, state) => const LoginScreen()),
    GoRoute(path: '/home',   builder: (ctx, state) => const HomeScreen()),
    GoRoute(path: '/profile',builder: (ctx, state) => const ProfileScreen()),
  ],
)

جعل الموجّه يتفاعل مع تغييرات المصادقة باستخدام refreshListenable

فحص مرة واحدة عند الإطلاق لا يكفي. عندما يسجل المستخدم دخوله أو خروجه، يجب أن يُعيد الموجّه تقييم دالة الاستدعاء redirect وأن يتنقل وفقاً لذلك. هنا يأتي دور refreshListenable. تُمرر أي Listenable — عادةً ChangeNotifier — لهذا المعامل. في كل مرة يُخطر فيها الـ Listenable مستمعيه، يستدعي GoRouter دالة redirect مجدداً للموقع الحالي. يُنشئ هذا تدفق مصادقة تفاعلياً بالكامل.

AuthNotifier مع GoRouter و refreshListenable

/// ChangeNotifier يكشف حالة المصادقة الحالية.
class AuthNotifier extends ChangeNotifier {
  bool _isLoggedIn = false;

  bool get isLoggedIn => _isLoggedIn;

  Future<void> logIn(String email, String password) async {
    // نفّذ استدعاء المصادقة الحقيقي هنا.
    await Future.delayed(const Duration(seconds: 1));
    _isLoggedIn = true;
    notifyListeners(); // <-- يطلق GoRouter لإعادة تشغيل redirect
  }

  Future<void> logOut() async {
    _isLoggedIn = false;
    notifyListeners(); // <-- يطلق GoRouter لإعادة تشغيل redirect
  }
}

// في إعداد تطبيقك:
final AuthNotifier _authNotifier = AuthNotifier();

final GoRouter router = GoRouter(
  initialLocation: '/home',
  refreshListenable: _authNotifier, // يُعيد تقييم redirect عند تغيير المصادقة
  redirect: (BuildContext context, GoRouterState state) {
    final bool loggedIn = _authNotifier.isLoggedIn;
    final String location = state.matchedLocation;

    final bool onLogin = location == '/login';
    final bool onSplash = location == '/';

    if (!loggedIn && !onLogin && !onSplash) {
      // احفظ الوجهة المقصودة كمعامل استعلام لإعادة التوجيه بعد الدخول.
      return '/login?from=${Uri.encodeComponent(location)}';
    }
    if (loggedIn && onLogin) return '/home';

    return null;
  },
  routes: [
    GoRoute(path: '/',       builder: (ctx, state) => const SplashScreen()),
    GoRoute(path: '/login',  builder: (ctx, state) => const LoginScreen()),
    GoRoute(path: '/home',   builder: (ctx, state) => const HomeScreen()),
    GoRoute(
      path: '/settings',
      builder: (ctx, state) => const SettingsScreen(),
    ),
    GoRoute(
      path: '/admin',
      builder: (ctx, state) => const AdminDashboard(),
      redirect: (ctx, state) {
        // حارس على مستوى المسار: يتطلب دور المسؤول.
        if (!_authNotifier.isLoggedIn) return '/login';
        if (!_authNotifier.isAdmin) return '/home';
        return null;
      },
    ),
  ],
);

redirect على مستوى المسار مقابل redirect على المستوى الأعلى

يدعم GoRouter دوال الاستدعاء redirect على مستويين:

  • redirect على المستوى الأعلى — مُعرَّف على نسخة GoRouter. يعمل لكل حدث تنقل عبر التطبيق بأكمله. مثالي لفحوصات المصادقة العامة.
  • redirect على مستوى المسار — مُعرَّف مباشرةً على GoRoute. يعمل فقط عند تطابق ذلك المسار المحدد. مثالي للتحكم الدقيق في الوصول المعتمد على الدور (مثل صفحات المسؤولين فقط).

يُنفَّذ كلا المستويين عند كل تنقل. إذا أعاد redirect المستوى الأعلى مساراً جديداً، يُعيد GoRouter التوجيه إليه دون تقييم redirect المسار الأصلي. يمكن للمستويين التعايش والتكامل.

إعادة التوجيه بعد تسجيل الدخول

يتذكر تدفق المصادقة المصقول المكان الذي كان المستخدم يتجه إليه قبل إعادة توجيهه إلى تسجيل الدخول، ويأخذه إليه بعد المصادقة الناجحة. النمط هو ترميز الوجهة المقصودة كمعامل استعلام (يُسمى عادةً from) عند التوجيه إلى /login. تقرأ LoginScreen هذا المعامل وتستدعي context.go(from) بعد تسجيل دخول ناجح، أو تعود إلى /home إذا غاب المعامل.

نصيحة: استخدم دائماً Uri.encodeComponent على مسار from قبل إلحاقه كقيمة استعلام. المسارات التي تحتوي على شرطات مائلة أو سلاسل استعلام خاصة بها ستكسر عنوان URL الأصلي بخلاف ذلك.

حالة المصادقة غير المتزامنة مع StreamProvider

عندما تأتي حالة مصادقتك من Stream (مثل Firebase Auth)، يمكنك تغليف الـ stream في StreamProvider أو ChangeNotifier مخصص يستمع إلى الـ stream ويستدعي notifyListeners() عند كل حدث. مرر ذلك الـ notifier كـ refreshListenable واقرأ القيمة الحالية داخل redirect. يضمن هذا استجابة GoRouter لأحداث تسجيل الدخول والخروج من أي مصدر — محلي أو OAuth أو بيومتري.

تحذير: لا تُجرِ أبداً عملاً غير متزامن (استدعاءات شبكة، قراءات قاعدة بيانات) مباشرةً داخل دالة الاستدعاء redirect. يجب أن تكون الدالة متزامنة. بدلاً من ذلك، احتفظ بحالة المصادقة في ChangeNotifier يتحدث بشكل غير متزامن في الخلفية ويُخطر GoRouter عند الجاهزية. قراءة قيمة محفوظة في الذاكرة داخل redirect آمنة وسريعة دائماً.

ملخص

دالة الاستدعاء redirect في GoRouter هي مكانك الوحيد المركزي لتطبيق قواعد المصادقة والتخويل عبر كل مسار في تطبيق Flutter. بإقرانها مع ChangeNotifier ممرَّر لـ refreshListenable، يتفاعل الموجّه تلقائياً في كل مرة تتغير فيها حالة مصادقة المستخدم — تسجيل الدخول يوجهه للرئيسية، وتسجيل الخروج يوجهه لتسجيل الدخول. تتيح لك عمليات redirect على مستوى المسار إضافة فحوصات دور دقيقة فوق الحارس العام. احرص دائماً على إبقاء دالة الاستدعاء متزامنة واعتمد على كائن حالة محفوظ في الذاكرة للحالة الحالية للمصادقة.